From e3569a06b2d9792e32ddaebf4a76d5c970919112 Mon Sep 17 00:00:00 2001 From: mindol1004 Date: Thu, 12 Dec 2024 13:48:05 +0900 Subject: [PATCH] commit --- .../domain/schedule/api/ScheduleJobApi.java | 22 ++++- .../dto/BatchJobHistoryProjection.java | 10 ++ .../BatchJobExecutionRepository.java | 15 ++- .../service/FindJobHistoryService.java | 30 ++++++ .../com/spring/infra/quartz/QuartzConfig.java | 2 +- .../java/com/spring/web/constant/Menus.java | 32 ++++--- .../web/controller/ScheduleController.java | 10 +- .../src/main/resources/application.yml | 11 ++- .../resources/static/js/apis/schedule-api.js | 7 ++ .../static/js/pages/schedule/history.js | 94 +++++++++++++++++++ .../templates/pages/schedule/history.html | 90 ++++++++++++++++++ 11 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobHistoryProjection.java create mode 100644 batch-quartz/src/main/java/com/spring/domain/schedule/service/FindJobHistoryService.java create mode 100644 batch-quartz/src/main/resources/static/js/pages/schedule/history.js create mode 100644 batch-quartz/src/main/resources/templates/pages/schedule/history.html diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java b/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java index 5d251c9..94f3932 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java @@ -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 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); + } + } diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobHistoryProjection.java b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobHistoryProjection.java new file mode 100644 index 0000000..bb1e813 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobHistoryProjection.java @@ -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(); +} diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java b/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java index bee1364..3171f34 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java @@ -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 { - + + @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 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 " + diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/service/FindJobHistoryService.java b/batch-quartz/src/main/java/com/spring/domain/schedule/service/FindJobHistoryService.java new file mode 100644 index 0000000..3e829fa --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/service/FindJobHistoryService.java @@ -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 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()); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java index b4b2923..0801a41 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java @@ -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; } diff --git a/batch-quartz/src/main/java/com/spring/web/constant/Menus.java b/batch-quartz/src/main/java/com/spring/web/constant/Menus.java index d0ec876..1b16151 100644 --- a/batch-quartz/src/main/java/com/spring/web/constant/Menus.java +++ b/batch-quartz/src/main/java/com/spring/web/constant/Menus.java @@ -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) ); diff --git a/batch-quartz/src/main/java/com/spring/web/controller/ScheduleController.java b/batch-quartz/src/main/java/com/spring/web/controller/ScheduleController.java index 0bd6dea..afcb86e 100644 --- a/batch-quartz/src/main/java/com/spring/web/controller/ScheduleController.java +++ b/batch-quartz/src/main/java/com/spring/web/controller/ScheduleController.java @@ -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"; + } + } diff --git a/batch-quartz/src/main/resources/application.yml b/batch-quartz/src/main/resources/application.yml index efeba64..b7df09b 100644 --- a/batch-quartz/src/main/resources/application.yml +++ b/batch-quartz/src/main/resources/application.yml @@ -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 diff --git a/batch-quartz/src/main/resources/static/js/apis/schedule-api.js b/batch-quartz/src/main/resources/static/js/apis/schedule-api.js index fed5341..6e709b8 100644 --- a/batch-quartz/src/main/resources/static/js/apis/schedule-api.js +++ b/batch-quartz/src/main/resources/static/js/apis/schedule-api.js @@ -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; } }; diff --git a/batch-quartz/src/main/resources/static/js/pages/schedule/history.js b/batch-quartz/src/main/resources/static/js/pages/schedule/history.js new file mode 100644 index 0000000..07bad6e --- /dev/null +++ b/batch-quartz/src/main/resources/static/js/pages/schedule/history.js @@ -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 = ` + ${history.jobName} + ${formatDateTime(history.startTime)} + ${formatDateTime(history.endTime)} + ${executionTime} ms + ${history.status} + `; + document.querySelector('tbody').appendChild(row); + }); +}; + +const handleScroll = () => { + const tableBody = document.getElementById('jobList'); + if (tableBody.scrollTop + tableBody.clientHeight >= tableBody.scrollHeight) { + fetchDataAndRender(); + } +}; diff --git a/batch-quartz/src/main/resources/templates/pages/schedule/history.html b/batch-quartz/src/main/resources/templates/pages/schedule/history.html new file mode 100644 index 0000000..d6b4207 --- /dev/null +++ b/batch-quartz/src/main/resources/templates/pages/schedule/history.html @@ -0,0 +1,90 @@ + + + + 잡 히스토리 + + +
+
+
+
+

잡 히스토리

+
+
+ +
+
+
+
+
+
+
+
+
+ 잡 검색 +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ 잡 목록 +
+
+ + + + + + + + + + + + +
잡 이름 시작 시간 종료 시간 수행 시간 상태
+
+
+
+
+
+
+ + +
+ + \ No newline at end of file