commit
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
package com.spring.domain.schedule.api;
|
package com.spring.domain.schedule.api;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
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.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.ReScheduleJobRequest;
|
||||||
import com.spring.domain.schedule.dto.ScheduleJobResponse;
|
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.FindScheduleJobService;
|
||||||
import com.spring.domain.schedule.service.ReScheduleJobService;
|
import com.spring.domain.schedule.service.ReScheduleJobService;
|
||||||
import com.spring.domain.schedule.service.ScheduleControlService;
|
import com.spring.domain.schedule.service.ScheduleControlService;
|
||||||
@@ -24,11 +30,13 @@ import lombok.RequiredArgsConstructor;
|
|||||||
@RequestMapping("/api/schedule")
|
@RequestMapping("/api/schedule")
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Validated
|
||||||
public class ScheduleJobApi {
|
public class ScheduleJobApi {
|
||||||
|
|
||||||
private final FindScheduleJobService findScheduleJobService;
|
private final FindScheduleJobService findScheduleJobService;
|
||||||
private final ReScheduleJobService reScheduleJobService;
|
private final ReScheduleJobService reScheduleJobService;
|
||||||
private final ScheduleControlService scheduleControlService;
|
private final ScheduleControlService scheduleControlService;
|
||||||
|
private final FindJobHistoryService findJobHistoryService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||||
@@ -66,4 +74,16 @@ public class ScheduleJobApi {
|
|||||||
return reScheduleJobService.rescheduleJob(request);
|
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;
|
package com.spring.domain.schedule.repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import com.spring.domain.schedule.dto.BatchJobAverageDurationProjection;
|
import com.spring.domain.schedule.dto.BatchJobAverageDurationProjection;
|
||||||
import com.spring.domain.schedule.dto.BatchJobExecutionProjection;
|
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.BatchJobHourProjection;
|
||||||
import com.spring.domain.schedule.dto.BatchJobStatusCountProjection;
|
import com.spring.domain.schedule.dto.BatchJobStatusCountProjection;
|
||||||
import com.spring.domain.schedule.entity.BatchJobExecution;
|
import com.spring.domain.schedule.entity.BatchJobExecution;
|
||||||
|
|
||||||
public interface BatchJobExecutionRepository extends JpaRepository<BatchJobExecution, Long> {
|
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, " +
|
@Query("SELECT bji.jobName AS jobName, " +
|
||||||
"bje.startTime AS startTime, " +
|
"bje.startTime AS startTime, " +
|
||||||
"bje.endTime AS endTime " +
|
"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.setDataSource(dataSource);
|
||||||
factory.setTransactionManager(transactionManager);
|
factory.setTransactionManager(transactionManager);
|
||||||
factory.setJobFactory(jobFactory);
|
factory.setJobFactory(jobFactory);
|
||||||
factory.setAutoStartup(true);
|
factory.setAutoStartup(false);
|
||||||
factory.setWaitForJobsToCompleteOnShutdown(true);
|
factory.setWaitForJobsToCompleteOnShutdown(true);
|
||||||
return factory;
|
return factory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,29 +12,35 @@ import lombok.RequiredArgsConstructor;
|
|||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum Menus {
|
public enum Menus {
|
||||||
|
|
||||||
DASHBOARD(
|
DASHBOARD(
|
||||||
"/dashboard",
|
"/dashboard",
|
||||||
"Dashboard",
|
"Dashboard",
|
||||||
"bi bi-grid",
|
"bi bi-grid",
|
||||||
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN, AgentUserRole.ROLE_USER)
|
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN, AgentUserRole.ROLE_USER)
|
||||||
),
|
),
|
||||||
SCHEDULE(
|
SCHEDULE(
|
||||||
"/schedule",
|
"/schedule",
|
||||||
"Schedule",
|
"Schedule",
|
||||||
"bi bi-menu-button-wide",
|
"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)
|
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN)
|
||||||
),
|
),
|
||||||
USER_MANAGEMENT(
|
USER_MANAGEMENT(
|
||||||
"/user/management",
|
"/user/management",
|
||||||
"User Management",
|
"User Management",
|
||||||
"bi bi-person",
|
"bi bi-person",
|
||||||
List.of(AgentUserRole.ROLE_SUPER)
|
List.of(AgentUserRole.ROLE_SUPER)
|
||||||
),
|
),
|
||||||
END_POINT(
|
END_POINT(
|
||||||
"/endpoint",
|
"/endpoint",
|
||||||
"API Explorer",
|
"API Explorer",
|
||||||
"bi bi-search",
|
"bi bi-search",
|
||||||
List.of(AgentUserRole.ROLE_SUPER)
|
List.of(AgentUserRole.ROLE_SUPER)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/schedule")
|
@RequestMapping("/schedule")
|
||||||
public class ScheduleController {
|
public class ScheduleController {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||||
public String schedule() {
|
public String schedule() {
|
||||||
return "pages/schedule/schedule";
|
return "pages/schedule/schedule";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/history")
|
||||||
|
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||||
|
public String history() {
|
||||||
|
return "pages/schedule/history";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
primary:
|
primary:
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: org.h2.Driver
|
||||||
url: 'jdbc:h2:mem:app'
|
url: 'jdbc:h2:file:D:/projects/h2-db/app'
|
||||||
username: mindol1004
|
username: test
|
||||||
password: 1111
|
password: 1111
|
||||||
hikari:
|
hikari:
|
||||||
pool-name: HikariPool-1
|
pool-name: HikariPool-1
|
||||||
@@ -24,8 +24,8 @@ spring:
|
|||||||
idle-timeout: 60000
|
idle-timeout: 60000
|
||||||
secondary:
|
secondary:
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: org.h2.Driver
|
||||||
url: 'jdbc:h2:mem:mob'
|
url: 'jdbc:h2:file:D:/projects/h2-db/mob'
|
||||||
username: mindol1004
|
username: test
|
||||||
password: 1111
|
password: 1111
|
||||||
hikari:
|
hikari:
|
||||||
pool-name: HikariPool-2
|
pool-name: HikariPool-2
|
||||||
@@ -44,7 +44,7 @@ spring:
|
|||||||
database-platform: org.hibernate.dialect.H2Dialect
|
database-platform: org.hibernate.dialect.H2Dialect
|
||||||
#show-sql: true
|
#show-sql: true
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: create
|
ddl-auto: update
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
dialect: org.hibernate.dialect.H2Dialect
|
dialect: org.hibernate.dialect.H2Dialect
|
||||||
@@ -168,6 +168,7 @@ management:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
root: info
|
||||||
org:
|
org:
|
||||||
hibernate:
|
hibernate:
|
||||||
SQL: debug
|
SQL: debug
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ const scheduleService = {
|
|||||||
refreshJob: async () => {
|
refreshJob: async () => {
|
||||||
await apiClient.post('/actuator/refresh');
|
await apiClient.post('/actuator/refresh');
|
||||||
return true;
|
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