feat: 재수집 실패 건 수 표시
This commit is contained in:
부모
43c28eeccd
커밋
eb8ed22139
3
.gitignore
vendored
3
.gitignore
vendored
@ -34,6 +34,9 @@ dependency-reduced-pom.xml
|
|||||||
buildNumber.properties
|
buildNumber.properties
|
||||||
.mvn/timing.properties
|
.mvn/timing.properties
|
||||||
.mvn/wrapper/maven-wrapper.jar
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
.mvn/wrapper/maven-wrapper.properties
|
||||||
|
mvnw
|
||||||
|
mvnw.cmd
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle/
|
.gradle/
|
||||||
|
|||||||
@ -287,6 +287,7 @@ export interface RecollectionSearchResponse {
|
|||||||
number: number;
|
number: number;
|
||||||
size: number;
|
size: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
|
failedRecordCounts: Record<number, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecollectionStatsResponse {
|
export interface RecollectionStatsResponse {
|
||||||
|
|||||||
@ -94,6 +94,9 @@ export default function Recollects() {
|
|||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [useSearch, setUseSearch] = useState(false);
|
const [useSearch, setUseSearch] = useState(false);
|
||||||
|
|
||||||
|
// 실패건 수 (jobExecutionId → count)
|
||||||
|
const [failedRecordCounts, setFailedRecordCounts] = useState<Record<number, number>>({});
|
||||||
|
|
||||||
// 실패 로그 모달
|
// 실패 로그 모달
|
||||||
const [failLogTarget, setFailLogTarget] = useState<RecollectionHistoryDto | null>(null);
|
const [failLogTarget, setFailLogTarget] = useState<RecollectionHistoryDto | null>(null);
|
||||||
|
|
||||||
@ -256,6 +259,7 @@ export default function Recollects() {
|
|||||||
setHistories(data.content);
|
setHistories(data.content);
|
||||||
setTotalPages(data.totalPages);
|
setTotalPages(data.totalPages);
|
||||||
setTotalCount(data.totalElements);
|
setTotalCount(data.totalElements);
|
||||||
|
setFailedRecordCounts(data.failedRecordCounts ?? {});
|
||||||
if (!useSearch) setPage(data.number);
|
if (!useSearch) setPage(data.number);
|
||||||
} catch {
|
} catch {
|
||||||
setHistories([]);
|
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">재수집 종료일시</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>
|
<th className="px-4 py-3 font-medium text-right">액션</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -727,6 +732,23 @@ export default function Recollects() {
|
|||||||
<td className="px-4 py-4 text-wing-muted whitespace-nowrap">
|
<td className="px-4 py-4 text-wing-muted whitespace-nowrap">
|
||||||
{formatDuration(hist.durationMs)}
|
{formatDuration(hist.durationMs)}
|
||||||
</td>
|
</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">
|
<td className="px-4 py-4 text-right">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/recollects/${hist.historyId}`)}
|
onClick={() => navigate(`/recollects/${hist.historyId}`)}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@ -497,12 +498,21 @@ public class BatchController {
|
|||||||
Page<BatchRecollectionHistory> histories = recollectionHistoryService
|
Page<BatchRecollectionHistory> histories = recollectionHistoryService
|
||||||
.getHistories(apiKey, jobName, status, from, to, PageRequest.of(page, size));
|
.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<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("content", histories.getContent());
|
response.put("content", histories.getContent());
|
||||||
response.put("totalElements", histories.getTotalElements());
|
response.put("totalElements", histories.getTotalElements());
|
||||||
response.put("totalPages", histories.getTotalPages());
|
response.put("totalPages", histories.getTotalPages());
|
||||||
response.put("number", histories.getNumber());
|
response.put("number", histories.getNumber());
|
||||||
response.put("size", histories.getSize());
|
response.put("size", histories.getSize());
|
||||||
|
response.put("failedRecordCounts", failedRecordCounts);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,14 @@ public interface BatchFailedRecordRepository extends JpaRepository<BatchFailedRe
|
|||||||
*/
|
*/
|
||||||
long countByJobExecutionId(Long jobExecutionId);
|
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로 벌크 업데이트
|
* 특정 Step 실행의 실패 레코드를 RESOLVED로 벌크 업데이트
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -330,6 +330,21 @@ public class RecollectionHistoryService {
|
|||||||
.build();
|
.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건
|
* 대시보드용 최근 10건
|
||||||
*/
|
*/
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user