feat: 재수집 실패 건 수 표시

This commit is contained in:
hyojin kim 2026-02-27 10:57:33 +09:00
부모 43c28eeccd
커밋 eb8ed22139
6개의 변경된 파일59개의 추가작업 그리고 0개의 파일을 삭제

3
.gitignore vendored
파일 보기

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

파일 보기

@ -287,6 +287,7 @@ export interface RecollectionSearchResponse {
number: number;
size: number;
totalPages: number;
failedRecordCounts: Record<number, number>;
}
export interface RecollectionStatsResponse {

파일 보기

@ -94,6 +94,9 @@ export default function Recollects() {
const [totalCount, setTotalCount] = useState(0);
const [useSearch, setUseSearch] = useState(false);
// 실패건 수 (jobExecutionId → count)
const [failedRecordCounts, setFailedRecordCounts] = useState<Record<number, number>>({});
// 실패 로그 모달
const [failLogTarget, setFailLogTarget] = useState<RecollectionHistoryDto | null>(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() {
<th className="px-4 py-3 font-medium"> </th>
<th className="px-4 py-3 font-medium"> </th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium text-center"></th>
<th className="px-4 py-3 font-medium text-right"></th>
</tr>
</thead>
@ -727,6 +732,23 @@ export default function Recollects() {
<td className="px-4 py-4 text-wing-muted whitespace-nowrap">
{formatDuration(hist.durationMs)}
</td>
<td className="px-4 py-4 text-center">
{(() => {
const count = hist.jobExecutionId
? (failedRecordCounts[hist.jobExecutionId] ?? 0)
: 0;
if (hist.executionStatus === 'STARTED') {
return <span className="text-xs text-wing-muted">-</span>;
}
return count > 0 ? (
<span className="inline-flex items-center px-2 py-0.5 text-xs font-semibold rounded-full bg-red-100 text-red-700">
{count}
</span>
) : (
<span className="text-xs text-wing-muted">0</span>
);
})()}
</td>
<td className="px-4 py-4 text-right">
<button
onClick={() => navigate(`/recollects/${hist.historyId}`)}

파일 보기

@ -28,6 +28,7 @@ import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Slf4j
@RestController
@ -497,12 +498,21 @@ public class BatchController {
Page<BatchRecollectionHistory> histories = recollectionHistoryService
.getHistories(apiKey, jobName, status, from, to, PageRequest.of(page, size));
// 목록의 jobExecutionId들로 실패건수 한번에 조회
List<Long> jobExecutionIds = histories.getContent().stream()
.map(BatchRecollectionHistory::getJobExecutionId)
.filter(Objects::nonNull)
.toList();
Map<Long, Long> failedRecordCounts = recollectionHistoryService
.getFailedRecordCounts(jobExecutionIds);
Map<String, Object> response = new HashMap<>();
response.put("content", histories.getContent());
response.put("totalElements", histories.getTotalElements());
response.put("totalPages", histories.getTotalPages());
response.put("number", histories.getNumber());
response.put("size", histories.getSize());
response.put("failedRecordCounts", failedRecordCounts);
return ResponseEntity.ok(response);
}

파일 보기

@ -33,6 +33,14 @@ public interface BatchFailedRecordRepository extends JpaRepository<BatchFailedRe
*/
long countByJobExecutionId(Long jobExecutionId);
/**
* 여러 jobExecutionId에 대해 FAILED 상태 건수를 한번에 조회 (N+1 방지)
*/
@Query("SELECT r.jobExecutionId, COUNT(r) FROM BatchFailedRecord r " +
"WHERE r.jobExecutionId IN :jobExecutionIds AND r.status = 'FAILED' " +
"GROUP BY r.jobExecutionId")
List<Object[]> countFailedByJobExecutionIds(@Param("jobExecutionIds") List<Long> jobExecutionIds);
/**
* 특정 Step 실행의 실패 레코드를 RESOLVED로 벌크 업데이트
*/

파일 보기

@ -330,6 +330,21 @@ public class RecollectionHistoryService {
.build();
}
/**
* 재수집 이력 목록의 jobExecutionId별 FAILED 상태 실패건수 조회
*/
@Transactional(readOnly = true)
public Map<Long, Long> getFailedRecordCounts(List<Long> jobExecutionIds) {
if (jobExecutionIds.isEmpty()) {
return Collections.emptyMap();
}
return failedRecordRepository.countFailedByJobExecutionIds(jobExecutionIds).stream()
.collect(Collectors.toMap(
row -> ((Number) row[0]).longValue(),
row -> ((Number) row[1]).longValue()
));
}
/**
* 대시보드용 최근 10건
*/