[agent-pipeline] pipe-20260318044621-56k5 round-5

This commit is contained in:
DDD1542 2026-03-18 14:07:14 +09:00
parent ab477abf8b
commit 577e9c12d1
2 changed files with 253 additions and 0 deletions

View File

@ -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 : "알 수 없는 오류",
});
}
}
}

View File

@ -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
*