[agent-pipeline] pipe-20260318044621-56k5 round-5
This commit is contained in:
parent
ab477abf8b
commit
577e9c12d1
|
|
@ -768,4 +768,238 @@ export class BatchManagementController {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 대시보드 통계 조회
|
||||
* GET /api/batch-management/stats
|
||||
* totalBatches, activeBatches, todayExecutions, todayFailures, prevDayExecutions, prevDayFailures
|
||||
* 멀티테넌시: company_code 필터링 필수
|
||||
*/
|
||||
static async getBatchStats(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
// 전체/활성 배치 수
|
||||
let configQuery: string;
|
||||
let configParams: any[] = [];
|
||||
if (companyCode === "*") {
|
||||
configQuery = `
|
||||
SELECT
|
||||
COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE is_active = 'Y')::int AS active
|
||||
FROM batch_configs
|
||||
`;
|
||||
} else {
|
||||
configQuery = `
|
||||
SELECT
|
||||
COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE is_active = 'Y')::int AS active
|
||||
FROM batch_configs
|
||||
WHERE company_code = $1
|
||||
`;
|
||||
configParams = [companyCode];
|
||||
}
|
||||
const configResult = await query<{ total: number; active: number }>(
|
||||
configQuery,
|
||||
configParams
|
||||
);
|
||||
|
||||
// 오늘/어제 실행·실패 수 (KST 기준 날짜)
|
||||
const logParams: any[] = [];
|
||||
let logWhere = "";
|
||||
if (companyCode && companyCode !== "*") {
|
||||
logWhere = " AND company_code = $1";
|
||||
logParams.push(companyCode);
|
||||
}
|
||||
const todayLogQuery = `
|
||||
SELECT
|
||||
COUNT(*)::int AS today_executions,
|
||||
COUNT(*) FILTER (WHERE execution_status = 'FAILED')::int AS today_failures
|
||||
FROM batch_execution_logs
|
||||
WHERE (start_time AT TIME ZONE 'Asia/Seoul')::date = (NOW() AT TIME ZONE 'Asia/Seoul')::date
|
||||
${logWhere}
|
||||
`;
|
||||
const prevDayLogQuery = `
|
||||
SELECT
|
||||
COUNT(*)::int AS prev_executions,
|
||||
COUNT(*) FILTER (WHERE execution_status = 'FAILED')::int AS prev_failures
|
||||
FROM batch_execution_logs
|
||||
WHERE (start_time AT TIME ZONE 'Asia/Seoul')::date = (NOW() AT TIME ZONE 'Asia/Seoul')::date - INTERVAL '1 day'
|
||||
${logWhere}
|
||||
`;
|
||||
const [todayResult, prevResult] = await Promise.all([
|
||||
query<{ today_executions: number; today_failures: number }>(
|
||||
todayLogQuery,
|
||||
logParams
|
||||
),
|
||||
query<{ prev_executions: number; prev_failures: number }>(
|
||||
prevDayLogQuery,
|
||||
logParams
|
||||
),
|
||||
]);
|
||||
|
||||
const config = configResult[0];
|
||||
const today = todayResult[0];
|
||||
const prev = prevResult[0];
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalBatches: config?.total ?? 0,
|
||||
activeBatches: config?.active ?? 0,
|
||||
todayExecutions: today?.today_executions ?? 0,
|
||||
todayFailures: today?.today_failures ?? 0,
|
||||
prevDayExecutions: prev?.prev_executions ?? 0,
|
||||
prevDayFailures: prev?.prev_failures ?? 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("배치 통계 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "배치 통계 조회 실패",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치별 최근 24시간 스파크라인 (1시간 단위 집계)
|
||||
* GET /api/batch-management/batch-configs/:id/sparkline
|
||||
* 멀티테넌시: company_code 필터링 필수
|
||||
*/
|
||||
static async getBatchSparkline(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user?.companyCode;
|
||||
const batchId = Number(id);
|
||||
if (!id || isNaN(batchId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "올바른 배치 ID를 제공해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
const params: any[] = [batchId];
|
||||
let companyFilter = "";
|
||||
if (companyCode && companyCode !== "*") {
|
||||
companyFilter = " AND bel.company_code = $2";
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
// KST 기준 최근 24시간 1시간 단위 슬롯 + 집계 (generate_series로 24개 보장)
|
||||
const sparklineQuery = `
|
||||
WITH kst_slots AS (
|
||||
SELECT to_char(s, 'YYYY-MM-DD"T"HH24:00:00') AS hour
|
||||
FROM generate_series(
|
||||
(NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '23 hours',
|
||||
(NOW() AT TIME ZONE 'Asia/Seoul'),
|
||||
INTERVAL '1 hour'
|
||||
) AS s
|
||||
),
|
||||
agg AS (
|
||||
SELECT
|
||||
to_char(date_trunc('hour', (bel.start_time AT TIME ZONE 'Asia/Seoul')) AT TIME ZONE 'Asia/Seoul', 'YYYY-MM-DD"T"HH24:00:00') AS hour,
|
||||
COUNT(*) FILTER (WHERE bel.execution_status = 'SUCCESS')::int AS success,
|
||||
COUNT(*) FILTER (WHERE bel.execution_status = 'FAILED')::int AS failed
|
||||
FROM batch_execution_logs bel
|
||||
WHERE bel.batch_config_id = $1
|
||||
AND bel.start_time >= (NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '24 hours'
|
||||
${companyFilter}
|
||||
GROUP BY date_trunc('hour', (bel.start_time AT TIME ZONE 'Asia/Seoul'))
|
||||
)
|
||||
SELECT
|
||||
k.hour,
|
||||
COALESCE(a.success, 0) AS success,
|
||||
COALESCE(a.failed, 0) AS failed
|
||||
FROM kst_slots k
|
||||
LEFT JOIN agg a ON k.hour = a.hour
|
||||
ORDER BY k.hour
|
||||
`;
|
||||
const data = await query<{
|
||||
hour: string;
|
||||
success: number;
|
||||
failed: number;
|
||||
}>(sparklineQuery, params);
|
||||
|
||||
return res.json({ success: true, data });
|
||||
} catch (error) {
|
||||
console.error("스파크라인 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "스파크라인 데이터 조회 실패",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치별 최근 실행 로그 (최대 20건)
|
||||
* GET /api/batch-management/batch-configs/:id/recent-logs
|
||||
* 멀티테넌시: company_code 필터링 필수
|
||||
*/
|
||||
static async getBatchRecentLogs(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user?.companyCode;
|
||||
const batchId = Number(id);
|
||||
const limit = Math.min(Number(req.query.limit) || 20, 20);
|
||||
if (!id || isNaN(batchId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "올바른 배치 ID를 제공해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
let logsQuery: string;
|
||||
let logsParams: any[];
|
||||
if (companyCode === "*") {
|
||||
logsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
start_time AS started_at,
|
||||
end_time AS finished_at,
|
||||
execution_status AS status,
|
||||
total_records,
|
||||
success_records,
|
||||
failed_records,
|
||||
error_message,
|
||||
duration_ms
|
||||
FROM batch_execution_logs
|
||||
WHERE batch_config_id = $1
|
||||
ORDER BY start_time DESC
|
||||
LIMIT $2
|
||||
`;
|
||||
logsParams = [batchId, limit];
|
||||
} else {
|
||||
logsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
start_time AS started_at,
|
||||
end_time AS finished_at,
|
||||
execution_status AS status,
|
||||
total_records,
|
||||
success_records,
|
||||
failed_records,
|
||||
error_message,
|
||||
duration_ms
|
||||
FROM batch_execution_logs
|
||||
WHERE batch_config_id = $1 AND company_code = $2
|
||||
ORDER BY start_time DESC
|
||||
LIMIT $3
|
||||
`;
|
||||
logsParams = [batchId, companyCode, limit];
|
||||
}
|
||||
|
||||
const result = await query(logsQuery, logsParams);
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error("최근 실행 이력 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "최근 실행 이력 조회 실패",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ import { authenticateToken } from "../middleware/authMiddleware";
|
|||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/batch-management/stats
|
||||
* 배치 대시보드 통계 (전체/활성 배치 수, 오늘·어제 실행/실패 수)
|
||||
* 반드시 /batch-configs 보다 위에 등록 (/:id로 잡히지 않도록)
|
||||
*/
|
||||
router.get("/stats", authenticateToken, BatchManagementController.getBatchStats);
|
||||
|
||||
/**
|
||||
* GET /api/batch-management/connections
|
||||
* 사용 가능한 커넥션 목록 조회
|
||||
|
|
@ -55,6 +62,18 @@ router.get("/batch-configs", authenticateToken, BatchManagementController.getBat
|
|||
*/
|
||||
router.get("/batch-configs/:id", authenticateToken, BatchManagementController.getBatchConfigById);
|
||||
|
||||
/**
|
||||
* GET /api/batch-management/batch-configs/:id/sparkline
|
||||
* 해당 배치 최근 24시간 1시간 단위 실행 집계
|
||||
*/
|
||||
router.get("/batch-configs/:id/sparkline", authenticateToken, BatchManagementController.getBatchSparkline);
|
||||
|
||||
/**
|
||||
* GET /api/batch-management/batch-configs/:id/recent-logs
|
||||
* 해당 배치 최근 실행 로그 (최대 20건)
|
||||
*/
|
||||
router.get("/batch-configs/:id/recent-logs", authenticateToken, BatchManagementController.getBatchRecentLogs);
|
||||
|
||||
/**
|
||||
* PUT /api/batch-management/batch-configs/:id
|
||||
* 배치 설정 업데이트
|
||||
|
|
|
|||
Loading…
Reference in New Issue