From eb8ed221394aa0dfb54c067beb107393c6a5648b Mon Sep 17 00:00:00 2001 From: hyojin kim Date: Fri, 27 Feb 2026 10:57:33 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9E=AC=EC=88=98=EC=A7=91=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EA=B1=B4=20=EC=88=98=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ frontend/src/api/batchApi.ts | 1 + frontend/src/pages/Recollects.tsx | 22 +++++++++++++++++++ .../global/controller/BatchController.java | 10 +++++++++ .../BatchFailedRecordRepository.java | 8 +++++++ .../service/RecollectionHistoryService.java | 15 +++++++++++++ 6 files changed, 59 insertions(+) diff --git a/.gitignore b/.gitignore index 3b66953..6993c75 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties .mvn/wrapper/maven-wrapper.jar +.mvn/wrapper/maven-wrapper.properties +mvnw +mvnw.cmd # Gradle .gradle/ diff --git a/frontend/src/api/batchApi.ts b/frontend/src/api/batchApi.ts index 18a186b..846b51f 100644 --- a/frontend/src/api/batchApi.ts +++ b/frontend/src/api/batchApi.ts @@ -287,6 +287,7 @@ export interface RecollectionSearchResponse { number: number; size: number; totalPages: number; + failedRecordCounts: Record; } export interface RecollectionStatsResponse { diff --git a/frontend/src/pages/Recollects.tsx b/frontend/src/pages/Recollects.tsx index 6b2b898..e6bbc09 100644 --- a/frontend/src/pages/Recollects.tsx +++ b/frontend/src/pages/Recollects.tsx @@ -94,6 +94,9 @@ export default function Recollects() { const [totalCount, setTotalCount] = useState(0); const [useSearch, setUseSearch] = useState(false); + // 실패건 수 (jobExecutionId → count) + const [failedRecordCounts, setFailedRecordCounts] = useState>({}); + // 실패 로그 모달 const [failLogTarget, setFailLogTarget] = useState(null); @@ -256,6 +259,7 @@ export default function Recollects() { setHistories(data.content); setTotalPages(data.totalPages); setTotalCount(data.totalElements); + setFailedRecordCounts(data.failedRecordCounts ?? {}); if (!useSearch) setPage(data.number); } catch { setHistories([]); @@ -684,6 +688,7 @@ export default function Recollects() { 재수집 시작일시 재수집 종료일시 소요시간 + 실패건 액션 @@ -727,6 +732,23 @@ export default function Recollects() { {formatDuration(hist.durationMs)} + + {(() => { + const count = hist.jobExecutionId + ? (failedRecordCounts[hist.jobExecutionId] ?? 0) + : 0; + if (hist.executionStatus === 'STARTED') { + return -; + } + return count > 0 ? ( + + {count}건 + + ) : ( + 0 + ); + })()} + + {/* Prev */} + + + {/* Page Numbers */} + {pages.map((p, idx) => + p === 'ellipsis' ? ( + + … + + ) : ( + + ), + )} + + {/* Next */} + + {/* Last */} + + + + ); +} diff --git a/frontend/src/pages/ExecutionDetail.tsx b/frontend/src/pages/ExecutionDetail.tsx index 34fd036..5a9a92a 100644 --- a/frontend/src/pages/ExecutionDetail.tsx +++ b/frontend/src/pages/ExecutionDetail.tsx @@ -6,6 +6,7 @@ import { usePoller } from '../hooks/usePoller'; import StatusBadge from '../components/StatusBadge'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; +import Pagination from '../components/Pagination'; const POLLING_INTERVAL_MS = 5000; @@ -92,7 +93,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { const fetchLogs = useCallback(async (p: number, s: ApiLogStatus) => { setLoading(true); try { - const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 50, status: s }); + const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 10, status: s }); setLogData(data); } catch { setLogData(null); @@ -163,7 +164,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { ) : logData && logData.content.length > 0 ? ( <> -
+
@@ -185,7 +186,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { key={log.logId} className={isError ? 'bg-red-50' : 'bg-white hover:bg-blue-50'} > - + + + {pagedRecords.map((record) => ( + + + + + + + + ))} + +
{page * 50 + idx + 1}{page * 10 + idx + 1}
@@ -225,33 +226,13 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) {
{/* 페이지네이션 */} - {logData.totalPages > 1 && ( -
- - {logData.totalElements.toLocaleString()}건 중{' '} - {(page * 50 + 1).toLocaleString()}~{Math.min((page + 1) * 50, logData.totalElements).toLocaleString()} - -
- - - {page + 1} / {logData.totalPages} - - -
-
- )} + ) : (

조회된 로그가 없습니다.

diff --git a/frontend/src/pages/RecollectDetail.tsx b/frontend/src/pages/RecollectDetail.tsx index 0524c19..3686f07 100644 --- a/frontend/src/pages/RecollectDetail.tsx +++ b/frontend/src/pages/RecollectDetail.tsx @@ -13,6 +13,7 @@ import { usePoller } from '../hooks/usePoller'; import StatusBadge from '../components/StatusBadge'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; +import Pagination from '../components/Pagination'; const POLLING_INTERVAL_MS = 10_000; @@ -97,7 +98,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { const fetchLogs = useCallback(async (p: number, s: ApiLogStatus) => { setLoading(true); try { - const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 50, status: s }); + const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 10, status: s }); setLogData(data); } catch { setLogData(null); @@ -168,7 +169,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { ) : logData && logData.content.length > 0 ? ( <> -
+
@@ -190,7 +191,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { key={log.logId} className={isError ? 'bg-red-50' : 'bg-white hover:bg-blue-50'} > - + + + {pagedRecords.map((record) => ( + + + + + + + + ))} + +
{page * 50 + idx + 1}{page * 10 + idx + 1}
@@ -230,33 +231,13 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) {
{/* 페이지네이션 */} - {logData.totalPages > 1 && ( -
- - {logData.totalElements.toLocaleString()}건 중{' '} - {(page * 50 + 1).toLocaleString()}~{Math.min((page + 1) * 50, logData.totalElements).toLocaleString()} - -
- - - {page + 1} / {logData.totalPages} - - -
-
- )} + ) : (

조회된 로그가 없습니다.

-- 2.45.2 From 1192a1117fed98475dd2177418e1e1d57e3bb76f Mon Sep 17 00:00:00 2001 From: hyojin kim Date: Fri, 27 Feb 2026 11:15:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=88=98=EC=A7=91=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EA=B1=B4=20=EB=A1=9C=EA=B7=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=EA=B8=B0=EB=8A=A5=20=EC=83=81=EC=84=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ExecutionDetail.tsx | 88 +++++++++++++++----------- frontend/src/pages/RecollectDetail.tsx | 88 +++++++++++++++----------- 2 files changed, 102 insertions(+), 74 deletions(-) diff --git a/frontend/src/pages/ExecutionDetail.tsx b/frontend/src/pages/ExecutionDetail.tsx index 5a9a92a..f5f5482 100644 --- a/frontend/src/pages/ExecutionDetail.tsx +++ b/frontend/src/pages/ExecutionDetail.tsx @@ -575,13 +575,18 @@ export default function ExecutionDetail() { ); } +const FAILED_PAGE_SIZE = 10; + function FailedRecordsToggle({ records, jobName, stepExecutionId }: { records: FailedRecordDto[]; jobName: string; stepExecutionId: number }) { const [open, setOpen] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const [retrying, setRetrying] = useState(false); + const [page, setPage] = useState(0); const navigate = useNavigate(); const failedRecords = records.filter((r) => r.status === 'FAILED'); + const totalPages = Math.ceil(records.length / FAILED_PAGE_SIZE); + const pagedRecords = records.slice(page * FAILED_PAGE_SIZE, (page + 1) * FAILED_PAGE_SIZE); const statusColor = (status: string) => { switch (status) { @@ -637,44 +642,53 @@ function FailedRecordsToggle({ records, jobName, stepExecutionId }: { records: F {open && ( -
- - - - - - - - - - - - {records.map((record) => ( - - - - - - +
+
+
Record Key에러 메시지재시도상태생성 시간
- {record.recordKey} - - {record.errorMessage || '-'} - - {record.retryCount} - - - {record.status} - - - {formatDateTime(record.createdAt)} -
+ + + + + + + - ))} - -
Record Key에러 메시지재시도상태생성 시간
+
+ {record.recordKey} + + {record.errorMessage || '-'} + + {record.retryCount} + + + {record.status} + + + {formatDateTime(record.createdAt)} +
+
+
)} diff --git a/frontend/src/pages/RecollectDetail.tsx b/frontend/src/pages/RecollectDetail.tsx index 3686f07..eb6235a 100644 --- a/frontend/src/pages/RecollectDetail.tsx +++ b/frontend/src/pages/RecollectDetail.tsx @@ -626,13 +626,18 @@ export default function RecollectDetail() { ); } +const FAILED_PAGE_SIZE = 10; + function FailedRecordsToggle({ records, jobName, stepExecutionId }: { records: FailedRecordDto[]; jobName: string; stepExecutionId: number }) { const [open, setOpen] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const [retrying, setRetrying] = useState(false); + const [page, setPage] = useState(0); const navigate = useNavigate(); const failedRecords = records.filter((r) => r.status === 'FAILED'); + const totalPages = Math.ceil(records.length / FAILED_PAGE_SIZE); + const pagedRecords = records.slice(page * FAILED_PAGE_SIZE, (page + 1) * FAILED_PAGE_SIZE); const statusColor = (status: string) => { switch (status) { @@ -688,44 +693,53 @@ function FailedRecordsToggle({ records, jobName, stepExecutionId }: { records: F {open && ( -
- - - - - - - - - - - - {records.map((record) => ( - - - - - - +
+
+
Record Key에러 메시지재시도상태생성 시간
- {record.recordKey} - - {record.errorMessage || '-'} - - {record.retryCount} - - - {record.status} - - - {formatDateTime(record.createdAt)} -
+ + + + + + + - ))} - -
Record Key에러 메시지재시도상태생성 시간
+
+ {record.recordKey} + + {record.errorMessage || '-'} + + {record.retryCount} + + + {record.status} + + + {formatDateTime(record.createdAt)} +
+
+
)} -- 2.45.2