commit
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, "접근이 금지되었습니다."),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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에서 제공)
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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'"
|
||||
}
|
||||
]}
|
||||
@@ -95,4 +95,9 @@ jwt:
|
||||
expiration: 1
|
||||
refresh-token:
|
||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
|
||||
expiration: 10080
|
||||
expiration: 10080
|
||||
|
||||
front:
|
||||
base:
|
||||
url: http://localhost:8081
|
||||
timeout: 100000
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) => {
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user