This commit is contained in:
mindol1004
2024-09-06 18:45:36 +09:00
parent a6a7de562e
commit 658f3d5543
27 changed files with 552 additions and 115 deletions

View File

@@ -18,7 +18,7 @@ import com.spring.common.error.GlobalExceptionHandler;
import com.spring.infra.security.error.SecurityExceptionHandler;
@RestControllerAdvice(
basePackages = "com.spring.domain.*.api",
basePackages = "com.spring.domain",
basePackageClasses = { GlobalExceptionHandler.class, SecurityExceptionHandler.class }
)
public class ResponseWrapper implements ResponseBodyAdvice<Object> {

View File

@@ -8,8 +8,8 @@ public class BizBaseException extends RuntimeException {
private final ExceptionRule exceptionRule;
public BizBaseException() {
super(ExceptionRule.BAD_REQUEST.getMessage());
this.exceptionRule = ExceptionRule.BAD_REQUEST;
super(ExceptionRule.SYSTE_ERROR.getMessage());
this.exceptionRule = ExceptionRule.SYSTE_ERROR;
}
public BizBaseException(ExceptionRule exceptionRule) {

View File

@@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum ExceptionRule implements ErrorRule {
SYSTE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "시스템 오류 입니다."),
BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "접근이 금지되었습니다."),

View File

@@ -0,0 +1,66 @@
package com.spring.domain.schedule.api;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.spring.domain.schedule.dto.ReScheduleJobRequest;
import com.spring.domain.schedule.dto.ScheduleJobResponse;
import com.spring.domain.schedule.service.FindJobGroupService;
import com.spring.domain.schedule.service.FindScheduleJobService;
import com.spring.domain.schedule.service.ReScheduleJobService;
import com.spring.domain.schedule.service.ScheduleControlService;
import lombok.RequiredArgsConstructor;
@RequestMapping("/api/schedule")
@RestController
@RequiredArgsConstructor
public class ScheduleJobApi {
private final FindScheduleJobService findScheduleJobService;
private final FindJobGroupService findJobGroupService;
private final ReScheduleJobService reScheduleJobService;
private final ScheduleControlService scheduleControlService;
@GetMapping
public List<ScheduleJobResponse> getAllJobs() {
return findScheduleJobService.getAllJobs();
}
@GetMapping("/groups")
public List<String> getJobGroups() {
return findJobGroupService.getJobGroups();
}
@GetMapping("/group/{groupName}/names")
public List<String> getJobNamesByGroup(@PathVariable String groupName) {
return findJobGroupService.getJobNamesByGroup(groupName);
}
@GetMapping("/pause/{groupName}/{jobName}")
public void pauseJob(@PathVariable String groupName, @PathVariable String jobName) {
scheduleControlService.pauseJob(groupName, jobName);
}
@GetMapping("/resume/{groupName}/{jobName}")
public void resumeJob(@PathVariable String groupName, @PathVariable String jobName) {
scheduleControlService.resumeJob(groupName, jobName);
}
@GetMapping("/trigger/{groupName}/{jobName}")
public void triggerJob(@PathVariable String groupName, @PathVariable String jobName) {
scheduleControlService.triggerJob(groupName, jobName);
}
@PostMapping("/reschedule")
public boolean rescheduleJob(@RequestBody ReScheduleJobRequest request) {
return reScheduleJobService.rescheduleJob(request);
}
}

View File

@@ -0,0 +1,14 @@
package com.spring.domain.schedule.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ReScheduleJobRequest {
private final String jobGroup;
private final String jobName;
private final String cronExpression;
}

View File

@@ -0,0 +1,23 @@
package com.spring.domain.schedule.dto;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ScheduleJobResponse {
private final String name; // 작업의 이름
private final String group; // 작업이 속한 그룹의 이름
private final String description; // 작업에 대한 설명 (JobDetail에서 제공)
private final String schedule; // 다음 실행 시간 (Trigger에서 제공)
private final String status; // 현재 트리거의 상태 (TriggerState에서 제공)
private final String jobClass; // 작업의 클래스 이름 (JobDetail에서 제공)
private final String jobDataMap; // 작업에 대한 데이터 맵 (JobDetail에서 제공, 직렬화된 형태로 저장)
private final String triggerType; // 트리거의 유형 (Trigger의 구현 클래스 이름)
private final LocalDateTime nextFireTime; // 다음 실행 시간 (Trigger에서 제공)
private final LocalDateTime previousFireTime; // 이전 실행 시간 (Trigger에서 제공)
}

View File

@@ -0,0 +1,40 @@
package com.spring.domain.schedule.service;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class FindJobGroupService {
private final Scheduler scheduler;
public List<String> getJobGroups() {
try {
return scheduler.getJobGroupNames();
} catch (SchedulerException e) {
return Collections.emptyList();
}
}
public List<String> getJobNamesByGroup(String groupName) {
try {
return scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))
.stream()
.map(JobKey::getName)
.collect(Collectors.toList());
} catch (SchedulerException e) {
return Collections.emptyList();
}
}
}

View File

@@ -0,0 +1,78 @@
package com.spring.domain.schedule.service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.stereotype.Service;
import com.spring.domain.schedule.dto.ScheduleJobResponse;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class FindScheduleJobService {
private final Scheduler scheduler;
public List<ScheduleJobResponse> getAllJobs() {
try {
return scheduler.getJobGroupNames().stream()
.flatMap(groupName -> {
try {
return scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName)).stream();
} catch (SchedulerException e) {
return Stream.empty();
}
})
.map(jobKey -> {
try {
return createSchedule(jobKey);
} catch (SchedulerException e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (SchedulerException e) {
return Collections.emptyList();
}
}
private ScheduleJobResponse createSchedule(JobKey jobKey) throws SchedulerException {
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
Trigger trigger = scheduler.getTriggersOfJob(jobKey).stream().findFirst().orElse(null);
return new ScheduleJobResponse(
jobKey.getName(),
jobKey.getGroup(),
jobDetail.getDescription(),
trigger != null ? trigger.getNextFireTime().toString() : "N/A",
trigger != null ? scheduler.getTriggerState(trigger.getKey()).name() : "UNKNOWN",
jobDetail.getJobClass().getName(),
jobDetail.getJobDataMap().toString(),
trigger != null ? trigger.getClass().getSimpleName() : "UNKNOWN",
trigger != null ? convertToLocalDateTime(trigger.getNextFireTime()) : null,
trigger != null ? convertToLocalDateTime(trigger.getPreviousFireTime()) : null
);
}
private LocalDateTime convertToLocalDateTime(Date date) {
if (date == null) return null;
return date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
}

View File

@@ -0,0 +1,39 @@
package com.spring.domain.schedule.service;
import org.quartz.CronScheduleBuilder;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.stereotype.Service;
import com.spring.domain.schedule.dto.ReScheduleJobRequest;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ReScheduleJobService {
private final Scheduler scheduler;
public boolean rescheduleJob(ReScheduleJobRequest request) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(request.getJobName(), request.getJobGroup());
Trigger oldTrigger = scheduler.getTrigger(triggerKey);
if (oldTrigger != null) {
Trigger newTrigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(CronScheduleBuilder.cronSchedule(request.getCronExpression()))
.build();
scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger);
return true;
}
return false;
} catch (SchedulerException e) {
return false;
}
}
}

View File

@@ -0,0 +1,45 @@
package com.spring.domain.schedule.service;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Service;
import com.spring.common.error.BizBaseException;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ScheduleControlService {
private final Scheduler scheduler;
public void pauseJob(String groupName, String jobName) {
try {
JobKey jobKey = new JobKey(jobName, groupName);
scheduler.pauseJob(jobKey);
} catch (SchedulerException e) {
throw new BizBaseException();
}
}
public void resumeJob(String groupName, String jobName) {
try {
JobKey jobKey = new JobKey(jobName, groupName);
scheduler.resumeJob(jobKey);
} catch (SchedulerException e) {
throw new BizBaseException();
}
}
public void triggerJob(String groupName, String jobName) {
try {
JobKey jobKey = new JobKey(jobName, groupName);
scheduler.triggerJob(jobKey);
} catch (SchedulerException e) {
throw new BizBaseException();
}
}
}

View File

@@ -26,6 +26,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.infra.security.filter.AuthenticationProcessingFilter;
import com.spring.infra.security.filter.JwtAuthenticationFilter;
import com.spring.infra.security.filter.RedirectIfAuthenticatedFilter;
import com.spring.infra.security.handler.SecurityAccessDeniedHandler;
import com.spring.infra.security.handler.SecurityAuthenticationEntryPoint;
import com.spring.infra.security.handler.SigninFailureHandler;
@@ -94,6 +95,9 @@ public class SecurityConfig {
.addFilterAfter(
new JwtAuthenticationFilter(tokenService, List.of(PERMITTED_URI)),
AuthenticationProcessingFilter.class
).addFilterAfter(
new RedirectIfAuthenticatedFilter(),
JwtAuthenticationFilter.class
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authenticationEntryPoint)

View File

@@ -0,0 +1,32 @@
package com.spring.infra.security.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
public class RedirectIfAuthenticatedFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String requestURI = request.getRequestURI();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && "/".equals(requestURI)) {
response.sendRedirect("/main");
return;
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,37 @@
package com.spring.infra.security.handler;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.ServletRequestPathUtils;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class HttpRequestEndpointChecker {
@Qualifier("requestMappingHandlerMapping")
private final RequestMappingHandlerMapping requestMapping;
public boolean isEndpointExist(HttpServletRequest request) {
try {
if (!ServletRequestPathUtils.hasParsedRequestPath(request)) {
ServletRequestPathUtils.parseAndCache(request);
}
HandlerExecutionChain handler = requestMapping.getHandler(request);
if (handler != null) {
HandlerMethod method = (HandlerMethod) handler.getHandler();
if (method != null) return true;
}
} catch (Exception e) {
return false;
}
return false;
}
}

View File

@@ -12,6 +12,8 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import lombok.RequiredArgsConstructor;
/**
* JWT 인증에서 인증되지 않은 접근을 처리하는 진입점 클래스입니다.
*
@@ -19,23 +21,27 @@ import org.springframework.web.servlet.HandlerExceptionResolver;
* @version 1.0
*/
@Component
@RequiredArgsConstructor
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final HandlerExceptionResolver resolver;
private static final String EXCEPTION_ATTRIBUTE = "exception";
public SecurityAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Qualifier("handlerExceptionResolver")
private final HandlerExceptionResolver resolver;
private final HttpRequestEndpointChecker endpointChecker;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
return;
if (!endpointChecker.isEndpointExist(request)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else {
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
} else {
resolver.resolveException(request, response, null, authException);
}
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}

View File

@@ -1,20 +1,22 @@
package com.spring.infra.security.handler;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.infra.security.jwt.JwtTokenService;
import lombok.RequiredArgsConstructor;
@@ -24,8 +26,8 @@ import lombok.RequiredArgsConstructor;
public class SigninSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenService jwtTokenService;
private final ObjectMapper objectMapper;
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(
@@ -35,14 +37,12 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
) throws IOException, ServletException {
jwtTokenService.generateAccessToken(response, authentication);
jwtTokenService.generateRefreshToken(response, authentication);
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) { // 접근 권한 없는 경로 접근해서 스프링 시큐리티가 인터셉트해서 로그인폼으로 이동 후 로그인 성공한 경우
redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
} else { // 로그인 버튼 눌러서 로그인한 경우 기존에 있던 페이지로 리다이렉트
String prevPage = String.valueOf(request.getSession().getAttribute("prevPage"));
redirectStrategy.sendRedirect(request, response, "null".equals(prevPage) ? "/main" : prevPage);
}
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : "/main";
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
Map<String, Object> responseBody = Map.of("status", true, "redirectUrl", targetUrl);
response.getWriter().write(objectMapper.writeValueAsString(responseBody));
}
}

View File

@@ -1,6 +1,5 @@
package com.spring.web.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -10,7 +9,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
public class MainController {
@GetMapping
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String main() {
return "pages/main/main";
}

View File

@@ -1,15 +1,25 @@
package com.spring.web.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
public class SignController {
@Value("${front.base.url}")
private String baseUrl;
@Value("${front.base.timeout}")
private int timeout;
@GetMapping
public String signIn() {
public String signIn(Model model) {
model.addAttribute("baseUrl", baseUrl);
model.addAttribute("timeout", timeout);
return "pages/sign/sign-in";
}

View File

@@ -153,5 +153,15 @@
"name": "spring.datasource.secondary.hikari.idle-timeout",
"type": "java.lang.String",
"description": "A description for 'spring.datasource.secondary.hikari.idle-timeout'"
},
{
"name": "front.base.url",
"type": "java.lang.String",
"description": "A description for 'front.base.url'"
},
{
"name": "front.base.timeout",
"type": "java.lang.String",
"description": "A description for 'front.base.timeout'"
}
]}

View File

@@ -95,4 +95,9 @@ jwt:
expiration: 1
refresh-token:
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
expiration: 10080
expiration: 10080
front:
base:
url: http://localhost:8081
timeout: 100000

View File

@@ -0,0 +1,31 @@
import apiClient from '../common/axios-instance.js';
export const getAllJobs = async () => {
const response = await apiClient.get('/api/schedule');
return response.data;
}
export const getJobGroups = async () => {
const response = await apiClient.get('/api/schedule/groups');
return response.data;
}
export const getJobNamesByGroup = async (groupName) => {
const response = await apiClient.get(`/api/schedule/group/${groupName}/names`);
return response.data;
}
export const pauseJob = async (groupName, jobName) => {
const response = await apiClient.post(`/api/schedule/pause/${groupName}/${jobName}`);
return response.data;
}
export const resumeJob = async (groupName, jobName) => {
const response = await apiClient.post(`/api/schedule/resume/${groupName}/${jobName}`);
return response.data;
}
export const triggerJob = async (groupName, jobName) => {
const response = await apiClient.post(`/api/schedule/trigger/${groupName}/${jobName}`);
return response.data;
};

View File

@@ -1,11 +1,8 @@
import apiClient from '../common/axios-instance.js';
export const signIn = async (username, password) => {
try {
await apiClient.post('/sign-in', {username, password});
} catch (error) {
console.error('Sign-in error:', error);
}
const response = await apiClient.post('/sign-in', {username, password});
return response.data;
};
export const signUp = async (loginId, password, userName) => {

View File

@@ -1,7 +1,7 @@
// Axios apiClient 생성
const apiClient = axios.create({
baseURL: 'http://localhost:8081', // 기본 URL 설정
timeout: 100000000, // 요청 타임아웃 설정 (10초)
baseURL: BASE_URL,
timeout: TIME_OUT,
headers: {
'Content-Type': 'application/json',
}
@@ -10,10 +10,6 @@ const apiClient = axios.create({
// 요청 인터셉터 추가
apiClient.interceptors.request.use(
(config) => {
const token = getAccessToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
@@ -27,61 +23,19 @@ apiClient.interceptors.response.use(
return response;
},
async (error) => {
const originalRequest = error.config;
// 401 Unauthorized 에러 처리
if (error.response && error.response.status === 401 && !originalRequest._retry) {
console.log("333333333333");
originalRequest._retry = true;
const refreshToken = getRefreshToken();
console.log("refreshToken===="+refreshToken);
if (refreshToken) {
try {
const response = await axios.post(baseURL+'/auth/refresh', {
refreshToken: refreshToken,
});
const { accessToken, newRefreshToken } = response.data;
// 새로 받은 토큰을 쿠키에 저장
setAccessToken(accessToken);
if (newRefreshToken) {
setRefreshToken(newRefreshToken);
}
// 갱신된 토큰으로 원래 요청 재시도
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
// refresh token이 만료된 경우 처리 (로그아웃 등)
console.error('Token refresh failed:', refreshError);
// 로그아웃 처리 또는 로그인 페이지로 리다이렉트
}
if (error.response) {
const status = error.response.status;
const message = error.response.data.message;
if (message) {
alert(message);
}
if (status == 401) {
location.href = "/";
}
return new Promise(() => {});
}
return Promise.reject(error);
}
);
// 쿠키에서 accessToken 가져오기
function getAccessToken() {
return Cookies.get('accessToken');
}
// 쿠키에서 refreshToken 가져오기
function getRefreshToken() {
return Cookies.get('refreshToken');
}
// 쿠키에 accessToken 저장
function setAccessToken(token) {
Cookies.set('accessToken', token, { secure: true, sameSite: 'strict' });
}
// 쿠키에 refreshToken 저장
function setRefreshToken(token) {
Cookies.set('refreshToken', token, { secure: true, sameSite: 'strict' });
}
export default apiClient;

View File

@@ -1,8 +1,13 @@
import {signIn} from '../../apis/user-api.js';
import {signIn} from '../../apis/sign-api.js';
document.getElementById('signinForm').addEventListener('submit', function(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
signIn(username, password);
signIn(username, password)
.then(response => {
if (response.status) {
window.location.href = response.redirectUrl;
}
});
});

View File

@@ -1,4 +1,4 @@
import {signUp} from '../../apis/user-api.js';
import {signUp} from '../../apis/sign-api.js';
document.getElementById('signupForm').addEventListener('submit', function(event) {
event.preventDefault();

View File

@@ -1,6 +1,10 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="config" lang="ko" xml:lang="ko">
<link rel="stylesheet" th:href="@{/css/style.css}">
<script>
const BASE_URL = /*[[${baseUrl}]]*/ '';
const TIME_OUT = /*[[${timeout}]]*/ '';
</script>
<script th:src="@{/js/lib/axios/axios.min.js}"></script>
<script th:src="@{/js/lib/cookie/js.cookie.min.js}"></script>
</html>

View File

@@ -0,0 +1,38 @@
<!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>Schedule - List</title>
</head>
<body>
<section layout:fragment="content">
<h2>Manage Batch Jobs</h2>
<table class="job-table">
<thead>
<tr>
<th>Job Name</th>
<th>Group</th>
<th>Schedule</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="job : ${jobs}">
<td th:text="${job.name}"></td>
<td th:text="${job.group}"></td>
<td th:text="${job.schedule}"></td>
<td th:text="${job.status}"></td>
<td>
<button class="btn" onclick="editJob('${job.name}', '${job.group}')">Edit</button>
<button class="btn danger" onclick="deleteJob('${job.name}', '${job.group}')">Delete</button>
</td>
</tr>
</tbody>
</table>
<button class="btn add-job" onclick="showAddJobForm()">Add Job</button>
</section>
</body>
</html>

View File

@@ -1,28 +1,28 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko" xml:lang="ko">
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
<link rel="stylesheet" href="/css/style.css">
<script src="/js/lib/axios/axios.min.js"></script>
<script src="/js/lib/cookie/js.cookie.min.js"></script>
<script type="module" src="/js/pages/sign/sign-up.js" defer></script>
</head>
<body>
<div class="container">
<h1>회원가입</h1>
<form id="signupForm" method="post">
<div class="input-group">
<input type="text" id="loginId" name="loginId" placeholder="아이디">
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/signin-layout}" lang="ko" xml:lang="ko">
<head>
<title>회원가입 페이지</title>
</head>
<body>
<section layout:fragment="content">
<div class="container">
<h1>회원가입</h1>
<form id="signupForm" method="post">
<div class="input-group">
<input type="text" id="loginId" name="loginId" placeholder="아이디">
</div>
<div class="input-group">
<input type="password" id="password" name="password" placeholder="비밀번호">
</div>
<div class="input-group">
<input type="text" id="userName" name="userName" placeholder="이름">
</div>
<button type="submit">회원가입</button>
</form>
</div>
<div class="input-group">
<input type="password" id="password" name="password" placeholder="비밀번호">
</div>
<div class="input-group">
<input type="text" id="userName" name="userName" placeholder="이름">
</div>
<button type="submit">회원가입</button>
</form>
</div>
</body>
<script type="module" th:src="@{/js/pages/sign/sign-up.js}" defer></script>
</section>
</body>
</html>