commit
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
package com.spring.domain.schedule.api;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -13,8 +17,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.spring.domain.schedule.dto.BatchJobHistoryProjection;
|
||||
import com.spring.domain.schedule.dto.ReScheduleJobRequest;
|
||||
import com.spring.domain.schedule.dto.ScheduleJobResponse;
|
||||
import com.spring.domain.schedule.service.FindJobHistoryService;
|
||||
import com.spring.domain.schedule.service.FindScheduleJobService;
|
||||
import com.spring.domain.schedule.service.ReScheduleJobService;
|
||||
import com.spring.domain.schedule.service.ScheduleControlService;
|
||||
@@ -24,11 +30,13 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequestMapping("/api/schedule")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
public class ScheduleJobApi {
|
||||
|
||||
|
||||
private final FindScheduleJobService findScheduleJobService;
|
||||
private final ReScheduleJobService reScheduleJobService;
|
||||
private final ScheduleControlService scheduleControlService;
|
||||
private final FindJobHistoryService findJobHistoryService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
@@ -66,4 +74,16 @@ public class ScheduleJobApi {
|
||||
return reScheduleJobService.rescheduleJob(request);
|
||||
}
|
||||
|
||||
@GetMapping("/history")
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
public List<BatchJobHistoryProjection> getJobHistory(
|
||||
@NotNull(message = "시작일자는 필수값 입니다.") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime fromDate,
|
||||
@NotNull(message = "종료일자는 필수값 입니다.") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime toDate,
|
||||
@RequestParam(required = false) String jobName,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size
|
||||
) {
|
||||
return findJobHistoryService.getJobHistory(fromDate, toDate, jobName, page, size);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.spring.domain.schedule.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public interface BatchJobHistoryProjection {
|
||||
String getJobName();
|
||||
LocalDateTime getStartTime();
|
||||
LocalDateTime getEndTime();
|
||||
String getStatus();
|
||||
}
|
||||
@@ -1,19 +1,32 @@
|
||||
package com.spring.domain.schedule.repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.spring.domain.schedule.dto.BatchJobAverageDurationProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobExecutionProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobHistoryProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobHourProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobStatusCountProjection;
|
||||
import com.spring.domain.schedule.entity.BatchJobExecution;
|
||||
|
||||
public interface BatchJobExecutionRepository extends JpaRepository<BatchJobExecution, Long> {
|
||||
|
||||
|
||||
@Query("SELECT bji.jobName AS jobName, " +
|
||||
"bje.startTime AS startTime, " +
|
||||
"bje.endTime AS endTime, " +
|
||||
"bje.status AS status " +
|
||||
"FROM BatchJobExecution bje " +
|
||||
"JOIN bje.jobInstance bji " +
|
||||
"WHERE bje.createTime BETWEEN :fromDate AND :toDate " +
|
||||
"ORDER BY bje.createTime DESC")
|
||||
List<BatchJobHistoryProjection> findJobHistory(@Param("fromDate") LocalDateTime fromDate, @Param("toDate") LocalDateTime toDate, Pageable pageable);
|
||||
|
||||
@Query("SELECT bji.jobName AS jobName, " +
|
||||
"bje.startTime AS startTime, " +
|
||||
"bje.endTime AS endTime " +
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.spring.domain.schedule.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.spring.domain.schedule.dto.BatchJobHistoryProjection;
|
||||
import com.spring.domain.schedule.repository.BatchJobExecutionRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FindJobHistoryService {
|
||||
|
||||
private final BatchJobExecutionRepository batchJobExecutionRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BatchJobHistoryProjection> getJobHistory(LocalDateTime fromDate, LocalDateTime toDate, String jobName, int page, int size) {
|
||||
return batchJobExecutionRepository.findJobHistory(fromDate, toDate, PageRequest.of(page, size)).stream()
|
||||
.filter(jobHistory -> !StringUtils.hasText(jobName) || jobHistory.getJobName().equals(jobName))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public class QuartzConfig {
|
||||
factory.setDataSource(dataSource);
|
||||
factory.setTransactionManager(transactionManager);
|
||||
factory.setJobFactory(jobFactory);
|
||||
factory.setAutoStartup(true);
|
||||
factory.setAutoStartup(false);
|
||||
factory.setWaitForJobsToCompleteOnShutdown(true);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -12,29 +12,35 @@ import lombok.RequiredArgsConstructor;
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Menus {
|
||||
|
||||
|
||||
DASHBOARD(
|
||||
"/dashboard",
|
||||
"Dashboard",
|
||||
"bi bi-grid",
|
||||
"/dashboard",
|
||||
"Dashboard",
|
||||
"bi bi-grid",
|
||||
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN, AgentUserRole.ROLE_USER)
|
||||
),
|
||||
SCHEDULE(
|
||||
"/schedule",
|
||||
"Schedule",
|
||||
"bi bi-menu-button-wide",
|
||||
"/schedule",
|
||||
"Schedule",
|
||||
"bi bi-menu-button-wide",
|
||||
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN)
|
||||
),
|
||||
JOB_HISTORY(
|
||||
"/schedule/history",
|
||||
"Job History",
|
||||
"bi bi-clock-history",
|
||||
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN)
|
||||
),
|
||||
USER_MANAGEMENT(
|
||||
"/user/management",
|
||||
"User Management",
|
||||
"bi bi-person",
|
||||
"/user/management",
|
||||
"User Management",
|
||||
"bi bi-person",
|
||||
List.of(AgentUserRole.ROLE_SUPER)
|
||||
),
|
||||
END_POINT(
|
||||
"/endpoint",
|
||||
"API Explorer",
|
||||
"bi bi-search",
|
||||
"/endpoint",
|
||||
"API Explorer",
|
||||
"bi bi-search",
|
||||
List.of(AgentUserRole.ROLE_SUPER)
|
||||
);
|
||||
|
||||
|
||||
@@ -8,11 +8,17 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@Controller
|
||||
@RequestMapping("/schedule")
|
||||
public class ScheduleController {
|
||||
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
public String schedule() {
|
||||
return "pages/schedule/schedule";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/history")
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
public String history() {
|
||||
return "pages/schedule/history";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ spring:
|
||||
datasource:
|
||||
primary:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: 'jdbc:h2:mem:app'
|
||||
username: mindol1004
|
||||
url: 'jdbc:h2:file:D:/projects/h2-db/app'
|
||||
username: test
|
||||
password: 1111
|
||||
hikari:
|
||||
pool-name: HikariPool-1
|
||||
@@ -24,8 +24,8 @@ spring:
|
||||
idle-timeout: 60000
|
||||
secondary:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: 'jdbc:h2:mem:mob'
|
||||
username: mindol1004
|
||||
url: 'jdbc:h2:file:D:/projects/h2-db/mob'
|
||||
username: test
|
||||
password: 1111
|
||||
hikari:
|
||||
pool-name: HikariPool-2
|
||||
@@ -44,7 +44,7 @@ spring:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
#show-sql: true
|
||||
hibernate:
|
||||
ddl-auto: create
|
||||
ddl-auto: update
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
@@ -168,6 +168,7 @@ management:
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
org:
|
||||
hibernate:
|
||||
SQL: debug
|
||||
|
||||
@@ -39,6 +39,13 @@ const scheduleService = {
|
||||
refreshJob: async () => {
|
||||
await apiClient.post('/actuator/refresh');
|
||||
return true;
|
||||
},
|
||||
|
||||
getJobHistory: async (fromDate, toDate, jobName, page, size) => {
|
||||
const response = await apiClient.get('/api/schedule/history', {
|
||||
params: { fromDate, toDate, jobName, page, size }
|
||||
});
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { addEvents, formatDateTime } from '../../common/common.js';
|
||||
import scheduleService from '../../apis/schedule-api.js';
|
||||
|
||||
let currentPage = 0;
|
||||
const pageSize = 50;
|
||||
let isLoading = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setDefaultDates();
|
||||
fetchDataAndRender();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
const setDefaultDates = () => {
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
document.getElementById('fromDate').value = today;
|
||||
document.getElementById('toDate').value = today;
|
||||
};
|
||||
|
||||
const setupEventListeners = () => {
|
||||
const eventMap = {
|
||||
'searchForm': { event: 'submit', handler: fetchData },
|
||||
'fromDate': { event: 'change', handler: validateDates },
|
||||
'toDate': { event: 'change', handler: validateDates }
|
||||
};
|
||||
addEvents(eventMap);
|
||||
};
|
||||
|
||||
const validateDates = () => {
|
||||
const fromDate = document.getElementById('fromDate');
|
||||
const toDate = document.getElementById('toDate');
|
||||
if (fromDate.value && toDate.value && fromDate.value > toDate.value) {
|
||||
alert('시작일자는 종료일자보다 클 수 없습니다.');
|
||||
fromDate.value = toDate.value;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
isLoading = false;
|
||||
currentPage = 0;
|
||||
document.getElementById('jobList').scrollTop = 0;
|
||||
document.querySelector('tbody').innerHTML = '';
|
||||
fetchDataAndRender();
|
||||
}
|
||||
|
||||
const fetchDataAndRender = async () => {
|
||||
const fromDateValue = document.getElementById('fromDate').value;
|
||||
const toDateValue = document.getElementById('toDate').value;
|
||||
if (!fromDateValue || !toDateValue) {
|
||||
alert('시작일자와 종료일자를 모두 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
|
||||
const jobName = document.getElementById('jobName').value;
|
||||
const fromDate = dayjs(document.getElementById('fromDate').value).format('YYYY-MM-DDTHH:mm:ss');
|
||||
const toDate = dayjs(document.getElementById('toDate').value).endOf('day').format('YYYY-MM-DDTHH:mm:ss');
|
||||
const jobHistories = await scheduleService.getJobHistory(fromDate, toDate, jobName, currentPage, pageSize);
|
||||
|
||||
if (jobHistories.length > 0) {
|
||||
appendJobHistories(jobHistories);
|
||||
currentPage++;
|
||||
isLoading = false;
|
||||
document.getElementById('jobList').addEventListener('scroll', handleScroll);
|
||||
} else {
|
||||
document.getElementById('jobList').removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
|
||||
const appendJobHistories = (jobHistories) => {
|
||||
jobHistories.forEach(history => {
|
||||
const row = document.createElement('tr');
|
||||
const startTime = dayjs(history.startTime);
|
||||
const endTime = dayjs(history.endTime);
|
||||
const executionTime = Math.abs(endTime.diff(startTime, 'millisecond'));
|
||||
row.innerHTML = `
|
||||
<td>${history.jobName}</td>
|
||||
<td>${formatDateTime(history.startTime)}</td>
|
||||
<td>${formatDateTime(history.endTime)}</td>
|
||||
<td>${executionTime} ms</td>
|
||||
<td>${history.status}</td>
|
||||
`;
|
||||
document.querySelector('tbody').appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const tableBody = document.getElementById('jobList');
|
||||
if (tableBody.scrollTop + tableBody.clientHeight >= tableBody.scrollHeight) {
|
||||
fetchDataAndRender();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layouts/layout}"
|
||||
layout:fragment="content" lang="ko" xml:lang="ko">
|
||||
<head>
|
||||
<title>잡 히스토리</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="main" class="main">
|
||||
<div class="pagetitle">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h1><i class="bi bi-clock-history"></i> 잡 히스토리</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<nav>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">홈</li>
|
||||
<li class="breadcrumb-item active">잡 히스토리</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="section">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title fs-6 text-dark">
|
||||
<i class="bi bi-search"></i> 잡 검색
|
||||
</h5>
|
||||
<form id="searchForm" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div class="form-floating">
|
||||
<input type="date" class="form-control" id="fromDate" placeholder="시작일자">
|
||||
<label for="fromDate"><i class="bi bi-calendar"></i> 시작일자</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-floating">
|
||||
<input type="date" class="form-control" id="toDate" placeholder="종료일자">
|
||||
<label for="toDate"><i class="bi bi-calendar"></i> 종료일자</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="jobName" placeholder="잡 이름">
|
||||
<label for="jobName"><i class="bi bi-briefcase"></i> 잡 이름</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary h-100 w-100">
|
||||
<i class="bi bi-search"></i> 검색
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title d-flex justify-content-between align-items-center">
|
||||
<span class="fs-6 text-dark"><i class="bi bi-list-ul"></i> 잡 목록</span>
|
||||
</h5>
|
||||
<div id="jobList" class="table-responsive" style="max-height: 480px; overflow-y: auto;">
|
||||
<table class="table table-hover">
|
||||
<thead style="position: sticky; top: 0; z-index: 1;">
|
||||
<tr>
|
||||
<th class="col-2 text-nowrap bg-light"><i class="bi bi-briefcase"></i> 잡 이름</th>
|
||||
<th class="col-2 text-nowrap bg-light"><i class="bi bi-calendar-event"></i> 시작 시간</th>
|
||||
<th class="col-2 text-nowrap bg-light"><i class="bi bi-calendar-event"></i> 종료 시간</th>
|
||||
<th class="col-2 text-nowrap bg-light"><i class="bi bi-clock"></i> 수행 시간</th>
|
||||
<th class="col-2 text-nowrap bg-light"><i class="bi bi-activity"></i> 상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="module" th:src="@{/js/pages/schedule/history.js}" defer></script>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user