This commit is contained in:
mindol1004
2024-09-26 18:40:15 +09:00
parent ec4f4f74c0
commit cf9d54ff85
26 changed files with 379 additions and 197 deletions

View File

@@ -1,34 +0,0 @@
package com.spring.domain.user.api;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
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.user.dto.SignInRequest;
import com.spring.domain.user.service.AuthService;
import com.spring.infra.security.jwt.JwtTokenService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class GenerateTokenApi {
private final AuthService authService;
private final JwtTokenService jwtTokenService;
@PostMapping("/auth")
public ResponseEntity<?> generateToken(HttpServletResponse response, @RequestBody SignInRequest request) {
Authentication auth = authService.getAuthentication(request.getUsername(), request.getPassword());
jwtTokenService.generateAccessToken(response, auth);
jwtTokenService.generateRefreshToken(response, auth);
return ResponseEntity.ok().body(null);
}
}

View File

@@ -15,7 +15,7 @@ import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class SignUpApi {
public class SignApi {
private final SignUpService signUpService;

View File

@@ -0,0 +1,33 @@
package com.spring.domain.user.entity;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "MEMBER")
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Member {
@Id
@Column(name = "MEMBER_ID", nullable = false)
private UUID memberId;
@Column(name = "LOGIN_ID", nullable = false, length = 50)
private String loginId;
@Column(name = "PASSWORD", nullable = false, length = 128)
private String password;
@Column(name = "MEMBER_NAME", nullable = false, length = 50)
private String userName;
}

View File

@@ -0,0 +1,51 @@
package com.spring.domain.user.entity;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MapsId;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "MEMBER_REFRESH_TOKEN")
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MemberRefreshToken {
@Id
@Column(name = "MEMBER_ID", nullable = false)
private UUID memberId;
@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "member_id")
private Member member;
@Column(name = "REFRESH_TOKEN", nullable = false)
private String refreshToken;
@Column(name = "REISSUE_COUNT", nullable = false)
private int reissueCount;
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public boolean validateRefreshToken(String refreshToken) {
return this.refreshToken.equals(refreshToken);
}
public void increaseReissueCount() {
reissueCount++;
}
}

View File

@@ -29,6 +29,7 @@ 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.SignOutHandler;
import com.spring.infra.security.handler.SigninFailureHandler;
import com.spring.infra.security.handler.SigninSuccessHandler;
import com.spring.infra.security.jwt.JwtTokenService;
@@ -82,6 +83,8 @@ public class SecurityConfig {
.anyRequest().authenticated()
)
.logout(logout -> logout
.logoutUrl("/sign-out")
.addLogoutHandler(new SignOutHandler(tokenService))
.logoutSuccessUrl("/")
.invalidateHttpSession(true))
.sessionManagement(session -> session

View File

@@ -1,14 +1,13 @@
package com.spring.infra.security.dto;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class SignInRequest {
private String username;
private String password;
private final String username;
private final String password;
}

View File

@@ -0,0 +1,17 @@
package com.spring.infra.security.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class SignResponse {
private final boolean status;
private final String redirectUrl;
public static SignResponse of(boolean status, String redirectUrl) {
return new SignResponse(status, redirectUrl);
}
}

View File

@@ -2,17 +2,18 @@ package com.spring.infra.security.filter;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import com.spring.infra.security.jwt.JwtTokenRule;
@@ -44,8 +45,6 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
* @param request HTTP 요청 객체
* @param response HTTP 응답 객체
* @param filterChain 필터 체인
* @throws ServletException 서블릿 예외
* @throws IOException 입출력 예외
*/
@Override
protected void doFilterInternal(
@@ -61,23 +60,20 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
}
try {
String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX);
String accessToken = parseBearerToken(request, HttpHeaders.AUTHORIZATION);
if (jwtTokenService.validateAccessToken(accessToken)) {
setAuthenticationToContext(accessToken);
return;
}
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
if (StringUtils.hasText(refreshToken)) {
if (validateToken(refreshToken, request)) {
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
jwtTokenService.generateRefreshToken(response, authentication);
setAuthenticationToContext(reissuedAccessToken);
}
} else {
jwtTokenService.deleteCookie(response);
}
jwtTokenService.validateToken(refreshToken);
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
jwtTokenService.generateRefreshToken(response, authentication);
setAuthenticationToContext(jwtTokenService.generateAccessToken(response, authentication));
} catch (Exception e) {
jwtTokenService.deleteCookie(response);
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
@@ -97,14 +93,11 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private boolean validateToken(final String token, HttpServletRequest request) {
try {
jwtTokenService.validateToken(token);
return true;
} catch (Exception e) {
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
return false;
}
private String parseBearerToken(HttpServletRequest request, String headerName) {
return Optional.ofNullable(request.getHeader(headerName))
.filter(token -> token.substring(0, 7).equalsIgnoreCase(JwtTokenRule.BEARER_PREFIX.getValue()))
.map(token -> token.substring(7))
.orElse(null);
}
}

View File

@@ -0,0 +1,23 @@
package com.spring.infra.security.handler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import com.spring.infra.security.jwt.JwtTokenService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class SignOutHandler implements LogoutHandler {
private final JwtTokenService jwtTokenService;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
jwtTokenService.deleteCookie(response);
}
}

View File

@@ -2,12 +2,12 @@ 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.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -17,6 +17,8 @@ import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.infra.security.dto.SignResponse;
import com.spring.infra.security.jwt.JwtTokenRule;
import com.spring.infra.security.jwt.JwtTokenService;
import lombok.RequiredArgsConstructor;
@@ -35,14 +37,18 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
HttpServletResponse response,
Authentication authentication
) throws IOException, ServletException {
jwtTokenService.generateAccessToken(response, authentication);
String accessToken = jwtTokenService.generateAccessToken(response, authentication);
jwtTokenService.generateRefreshToken(response, authentication);
SavedRequest savedRequest = requestCache.getRequest(request, response);
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : "/dashboard";
response.setHeader(HttpHeaders.AUTHORIZATION, JwtTokenRule.BEARER_PREFIX.getValue() + accessToken);
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));
response.getWriter().write(objectMapper.writeValueAsString(SignResponse.of(true, targetUrl)));
}
}

View File

@@ -1,8 +1,6 @@
package com.spring.infra.security.jwt;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
@@ -42,7 +40,7 @@ public class JwtTokenGenerator {
.setHeader(createHeader())
.setClaims(createClaims(authentication))
.setSubject(authentication.getName())
.setIssuedAt(Timestamp.valueOf(LocalDateTime.now()))
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(Instant.now().plus(jwtProperties.getAccessToken().getExpiration(), ChronoUnit.MINUTES)))
.signWith(jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret()))
.compact();
@@ -56,8 +54,8 @@ public class JwtTokenGenerator {
*/
public String generateRefreshToken(Authentication authentication) {
return Jwts.builder()
.setHeader(createHeader())
.setSubject(authentication.getName())
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(Instant.now().plus(jwtProperties.getRefreshToken().getExpiration(), ChronoUnit.MINUTES)))
.signWith(jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret()))
.compact();

View File

@@ -20,11 +20,6 @@ public enum JwtTokenRule {
*/
JWT_ISSUE_HEADER("Set-Cookie"),
/**
* JWT 토큰 해석 시 사용되는 HTTP 헤더 이름입니다.
*/
JWT_RESOLVE_HEADER("Cookie"),
/**
* 액세스 토큰의 접두사입니다.
*/

View File

@@ -8,6 +8,7 @@ import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -39,7 +40,6 @@ public class JwtTokenService {
private final UserPrincipalService userPrincipalService;
private final Key accessSecretKey;
private final Key refreshSecretKey;
private final long accessExpiration;
private final long refreshExpiration;
public JwtTokenService(
@@ -53,7 +53,6 @@ public class JwtTokenService {
this.userPrincipalService = userPrincipalService;
this.accessSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret());
this.refreshSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret());
this.accessExpiration = jwtProperties.getAccessToken().getExpiration();
this.refreshExpiration = jwtProperties.getRefreshToken().getExpiration();
}
@@ -66,13 +65,12 @@ public class JwtTokenService {
*/
public String generateAccessToken(HttpServletResponse response, Authentication authentication) {
String accessToken = jwtTokenGenerator.generateAccessToken(authentication);
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, accessExpiration);
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
response.setHeader(HttpHeaders.AUTHORIZATION, JwtTokenRule.BEARER_PREFIX.getValue() + accessToken);
return accessToken;
}
/**
* 리프레시 토큰을 생성하고 응답 헤더에 설정합니다.
* 리프레시 토큰을 생성하고 쿠키에 저장한다.
*
* @param response HTTP 응답 객체
* @param authentication 인증 정보
@@ -96,7 +94,7 @@ public class JwtTokenService {
private ResponseCookie setTokenToCookie(String tokenPrefix, String token, long maxAgeSeconds) {
return ResponseCookie.from(tokenPrefix, token)
.path("/")
.maxAge(maxAgeSeconds)
.maxAge(maxAgeSeconds * 60)
.httpOnly(true)
.sameSite("Lax")
.secure(false)
@@ -208,14 +206,12 @@ public class JwtTokenService {
}
/**
* 액세스 토큰과 리프레시 토큰 쿠키를 삭제합니다.
* 리프레시 토큰 쿠키를 삭제합니다.
*
* @param response HTTP 응답 객체
*/
public void deleteCookie(HttpServletResponse response) {
Cookie accessCookie = jwtTokenUtil.resetToken(JwtTokenRule.ACCESS_PREFIX);
Cookie refreshCookie = jwtTokenUtil.resetToken(JwtTokenRule.REFRESH_PREFIX);
response.addCookie(accessCookie);
response.addCookie(refreshCookie);
}

View File

@@ -105,50 +105,55 @@ h6 {
--------------------------------------------------------------*/
/* Dropdown menus */
.dropdown-menu {
border-radius: 4px;
padding: 10px 0;
border-radius: 0.5rem; /* 모서리 둥글게 */
padding: 0; /* 패딩 제거 */
animation-name: dropdown-animate;
animation-duration: 0.2s;
animation-fill-mode: both;
border: 0;
box-shadow: 0 5px 30px 0 rgba(82, 63, 105, 0.2);
border: 1px solid rgba(0, 0, 0, 0.1); /* 경계선 추가 */
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); /* 부드러운 그림자 효과 */
background-color: #ffffff; /* 배경색 설정 */
}
.dropdown-menu .dropdown-header,
.dropdown-menu .dropdown-footer {
text-align: center;
font-size: 15px;
padding: 10px 25px;
font-size: 16px; /* 폰트 크기 조정 */
padding: 10px 15px; /* 패딩 조정 */
color: #495057; /* 텍스트 색상 */
font-weight: 600; /* 두꺼운 텍스트 */
}
.dropdown-menu .dropdown-footer a {
color: #444444;
color: #007bff; /* 링크 색상 */
text-decoration: underline;
}
.dropdown-menu .dropdown-footer a:hover {
text-decoration: none;
text-decoration: none; /* 호버 시 밑줄 제거 */
color: #0056b3; /* 호버 시 링크 색상 변경 */
}
.dropdown-menu .dropdown-divider {
color: #a5c5fe;
margin: 0;
color: rgba(0, 0, 0, 0.1); /* 경계선 색상 */
margin: 0; /* 마진 제거 */
}
.dropdown-menu .dropdown-item {
font-size: 14px;
padding: 10px 15px;
transition: 0.3s;
font-size: 14px; /* 텍스트 크기 */
padding: 10px 15px; /* 패딩 조정 */
transition: background-color 0.3s, color 0.3s; /* 부드러운 전환 효과 */
color: #212529; /* 기본 텍스트 색상 */
}
.dropdown-menu .dropdown-item i {
margin-right: 10px;
font-size: 18px;
line-height: 0;
margin-right: 10px; /* 아이콘과 텍스트 간격 */
font-size: 18px; /* 아이콘 크기 */
}
.dropdown-menu .dropdown-item:hover {
background-color: #f6f9ff;
background-color: #f1f1f1; /* 호버 시 배경색 */
color: #000; /* 호버 시 텍스트 색상 */
}
@media (min-width: 768px) {
@@ -156,13 +161,13 @@ h6 {
content: "";
width: 13px;
height: 13px;
background: #fff;
background: #ffffff; /* 화이트 배경 */
position: absolute;
top: -7px;
right: 20px;
transform: rotate(45deg);
border-top: 1px solid #eaedf1;
border-left: 1px solid #eaedf1;
border-top: 1px solid rgba(0, 0, 0, 0.1); /* 경계선 색상 */
border-left: 1px solid rgba(0, 0, 0, 0.1); /* 경계선 색상 */
}
}
@@ -612,6 +617,50 @@ h6 {
background-color: #fff;
}
/* 큰 화면에서는 사이드바가 항상 보이도록 설정 */
@media (min-width: 1200px) {
.sidebar {
left: 0; /* 사이드바가 항상 화면에 나타남 */
transition: left 0.3s;
}
/* 사이드바가 열리거나 닫히는 효과는 큰 화면에서 적용하지 않음 */
.toggle-sidebar .sidebar {
left: 0;
}
/* 큰 화면에서는 메인 콘텐츠가 사이드바 크기만큼 옆으로 이동 */
#main,
#footer {
margin-left: 300px;
transition: margin-left 0.3s;
}
}
/* 작은 화면에서는 사이드바가 숨겨지고, 버튼으로 열림 */
@media (max-width: 1199px) {
.sidebar {
left: -300px;
transition: left 0.3s;
position: fixed;
top: 60px;
width: 300px;
height: calc(100% - 60px);
z-index: 999;
}
/* toggle-sidebar 클래스가 추가되면 사이드바가 나타남 */
.toggle-sidebar .sidebar {
left: 0;
}
/* 작은 화면에서는 메인 콘텐츠가 사이드바와 관계없이 유지됨 */
#main,
#footer {
margin-left: 0;
}
}
@media (max-width: 1199px) {
.sidebar {
left: -300px;
@@ -629,7 +678,6 @@ h6 {
}
@media (min-width: 1200px) {
#main,
#footer {
margin-left: 300px;

View File

@@ -1,13 +1,19 @@
import apiClient from '../common/axios-instance.js';
export const getBatchJobExecutionData = async (year, month) => {
const response = await apiClient.get('/api/dashboard/chart', {
params: { year, month }
});
return response.data;
}
const dashBoardService = {
export const getRecentJobs = async () => {
const response = await apiClient.get('/api/dashboard/recent-job');
return response.data;
}
getBatchJobExecutionData: async (year, month) => {
const response = await apiClient.get('/api/dashboard/chart', {
params: { year, month }
});
return response.data.data;
},
getRecentJobs: async () => {
const response = await apiClient.get('/api/dashboard/recent-job');
return response.data.data;
}
};
export default dashBoardService;

View File

@@ -1,35 +1,42 @@
import apiClient from '../common/axios-instance.js';
export const getAllJobs = async (searchParams) => {
const response = await apiClient.get('/api/schedule', { params: searchParams });
return response.data;
}
const scheduleService = {
export const getJobDetail = async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/${groupName}/${jobName}`);
return response.data;
}
getAllJobs: async (searchParams) => {
const response = await apiClient.get('/api/schedule', { params: searchParams });
return response.data.data;
},
getJobDetail: async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/${groupName}/${jobName}`);
return response.data.data;
},
pauseJob: async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/pause/${groupName}/${jobName}`);
return response.data.data;
},
resumeJob: async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/resume/${groupName}/${jobName}`);
return response.data.data;
},
triggerJob: async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/trigger/${groupName}/${jobName}`);
return response.data.data;
},
rescheduleJob: async (jobGroup, jobName, cronExpression) => {
const response = await apiClient.post('/api/schedule/reschedule', {
jobGroup,
jobName,
cronExpression
});
return response.data.data;
}
export const pauseJob = async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/pause/${groupName}/${jobName}`);
return response.data;
}
export const resumeJob = async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/resume/${groupName}/${jobName}`);
return response.data;
}
export const triggerJob = async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/trigger/${groupName}/${jobName}`);
return response.data;
};
export const rescheduleJob = async (jobGroup, jobName, cronExpression) => {
const response = await apiClient.post('/api/schedule/reschedule', {
jobGroup,
jobName,
cronExpression
});
return response.data;
};
export default scheduleService;

View File

@@ -1,11 +1,24 @@
import apiClient from '../common/axios-instance.js';
import apiClient, { saveAccessToken, removeTokens } from '../common/axios-instance.js';
const signService = {
signIn: async (username, password) => {
const response = await apiClient.post('/sign-in', { username, password });
const accessToken = response.headers['authorization'].split(' ')[1];
saveAccessToken(accessToken);
return response.data;
},
signOut: async () => {
await apiClient.post('/sign-out');
removeTokens();
},
signUp: async (loginId, password, userName) => {
const response = await apiClient.post('/api/user/sign-up', { loginId, password, userName });
return response.data;
}
export const signIn = async (username, password) => {
const response = await apiClient.post('/sign-in', {username, password});
return response;
};
export const signUp = async (loginId, password, userName) => {
const response = await apiClient.post('/api/user/sign-up', {loginId, password, userName});
return response.data;
};
export default signService;

View File

@@ -1,5 +1,26 @@
const baseUrl = window.BASE_URL || '';
const timeOut = window.TIME_OUT || 5000;
const timeOut = window.TIME_OUT || 100000;
const setAuthorizationHeader = (token) => {
if (token) {
apiClient.defaults.headers['Authorization'] = `Bearer ${token}`;
} else {
delete apiClient.defaults.headers['Authorization'];
}
};
const getAccessToken = () => localStorage.getItem('accessToken');
export const saveAccessToken = (token) => {
localStorage.setItem('accessToken', token);
setAuthorizationHeader(token);
};
export const removeTokens = () => {
localStorage.removeItem('accessToken');
setAuthorizationHeader(null);
location.href = "/";
};
// Axios apiClient 생성
const apiClient = axios.create({
@@ -7,21 +28,24 @@ const apiClient = axios.create({
timeout: timeOut,
headers: {
'Content-Type': 'application/json',
}
},
withCredentials: true
});
apiClient.interceptors.request.use(
(config) => {
const token = getAccessToken();
if (token) {
setAuthorizationHeader(token);
}
return config;
},
(error) => {
return Promise.reject(error);
}
(error) => Promise.reject(error)
);
apiClient.interceptors.response.use(
(response) => {
return response.data;
return response;
},
async (error) => {
if (error.response) {

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
/*! js-cookie v3.0.5 | MIT */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}var t=function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"});return t}));

View File

@@ -1,5 +1,5 @@
import { formatDateTime } from '../../common/common.js';
import { getBatchJobExecutionData, getRecentJobs } from '../../apis/dashboard-api.js';
import dashBoardService from '../../apis/dashboard-api.js';
let selectedMonth;
document.addEventListener('DOMContentLoaded', () => {
@@ -26,8 +26,8 @@ const initMonthPicker = () => {
const fetchDataAndRender = async () => {
const [year, month] = selectedMonth.split('-');
const batchData = await getBatchJobExecutionData(year, month);
const recentJobs = await getRecentJobs();
const batchData = await dashBoardService.getBatchJobExecutionData(year, month);
const recentJobs = await dashBoardService.getRecentJobs();
renderBatchExecutionTimeChart(batchData.jobAvgSummary);
renderBatchStatusChart(batchData.statusCounts);

View File

@@ -0,0 +1,16 @@
import signService from '../../apis/sign-api.js';
document.addEventListener('DOMContentLoaded', () => {
const signOutButton = document.getElementById('signOut');
const toggleSidebar = document.getElementById('toggleSidebar');
signOutButton.addEventListener('click', (e) => {
e.preventDefault();
signService.signOut();
});
toggleSidebar.addEventListener('click', (e) => {
const body = document.body;
body.classList.toggle("toggle-sidebar");
});
});

View File

@@ -1,5 +1,5 @@
import { formatDateTime } from '../../common/common.js';
import { getAllJobs, getJobDetail, pauseJob, resumeJob, rescheduleJob } from '../../apis/schedule-api.js';
import scheduleService from '../../apis/schedule-api.js';
document.addEventListener('DOMContentLoaded', () => {
fetchDataAndRender();
@@ -14,7 +14,7 @@ const fetchDataAndRender = async () => {
const searchForm = document.getElementById('searchForm');
const formData = new FormData(searchForm);
const searchParams = new URLSearchParams(formData);
const response = await getAllJobs(searchParams);
const response = await scheduleService.getAllJobs(searchParams);
updateTable(response);
};
@@ -41,7 +41,7 @@ const updateTable = (jobs) => {
const showJobDetail = async (e) => {
const { group, name } = e.target.closest('button').dataset;
const jobDetail = await getJobDetail(group, name);
const jobDetail = await scheduleService.getJobDetail(group, name);
const detailContent = document.getElementById('scheduleDetailContent');
detailContent.innerHTML = `
<div class="card">
@@ -102,7 +102,7 @@ const getStatusBadgeClass = (status) => {
const updateCronExpression = async (group, name) => {
const cronExpressionInput = document.getElementById('cronExpression');
const newCronExpression = cronExpressionInput.value;
const result = await rescheduleJob(group, name, newCronExpression);
const result = await scheduleService.rescheduleJob(group, name, newCronExpression);
if (result) {
alert('스케쥴이 수정 되었습니다.');
fetchDataAndRender();
@@ -144,7 +144,7 @@ const updateJobControlButtons = (status) => {
};
const updateJobStatus = async (group, name, newStatus) => {
const jobDetail = await getJobDetail(group, name);
const jobDetail = await scheduleService.getJobDetail(group, name);
jobDetail.status = newStatus;
const statusElement = document.querySelector('#scheduleDetailContent .badge');

View File

@@ -1,4 +1,4 @@
import {signIn, signUp} from '../../apis/sign-api.js';
import signService from '../../apis/sign-api.js';
document.addEventListener('DOMContentLoaded', () => {
@@ -11,8 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
signIn(username, password).then(response => {
console.log(response);
signService.signIn(username, password).then(response => {
if (response.status) {
window.location.href = response.redirectUrl;
}
@@ -29,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
const loginId = document.getElementById('loginId').value;
const password = document.getElementById('loginPassword').value;
const userName = document.getElementById('userName').value;
signUp(loginId, password, userName).then(response => {
signService.signUp(loginId, password, userName).then(() => {
alert(`회원가입이 완료 되었습니다.`);
signupModal.hide();
});

View File

@@ -8,10 +8,8 @@
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>
<script th:src="@{/js/lib/bootstrap/popper.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/bootstrap.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/chart.js}"></script>
<script th:src="@{/js/lib/bootstrap/luxon.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/chartjs-adapter-luxon.umd.min.js}"></script>

View File

@@ -5,28 +5,28 @@
<a href="index.html" class="logo d-flex align-items-center">
<span class="d-none d-lg-block">NXCUS - Agent2.0</span>
</a>
<i class="bi bi-list toggle-sidebar-btn"></i>
</div>
<div class="search-bar">
<form class="search-form d-flex align-items-center" method="POST" action="#">
<input type="text" name="query" placeholder="Search" title="Enter search keyword">
<button type="submit" title="Search"><i class="bi bi-search"></i></button>
</form>
<button class="btn toggle-sidebar-btn" id="toggleSidebar" aria-label="Toggle Sidebar">
<i class="bi bi-list"></i>
</button>
</div>
<nav class="header-nav ms-auto">
<ul class="d-flex align-items-center">
<li class="nav-item d-block d-lg-none">
<a class="nav-link nav-icon search-bar-toggle " href="#">
<i class="bi bi-search"></i>
</a>
</li>
<li class="nav-item dropdown pe-3">
<a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown">
<img src="/images/user-id.png" alt="Profile" class="rounded-circle">
<span class="d-none d-md-block dropdown-toggle ps-2">K. Anderson</span>
</a>
<button class="nav-link nav-profile d-flex align-items-center pe-0 dropdown-toggle" id="profileDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i>
<span class="d-none d-md-block ps-2">K. Anderson</span>
</button>
<ul class="dropdown-menu dropdown-menu-end w-auto" aria-labelledby="profileDropdown">
<li>
<button class="dropdown-item" id="signOut" style="display: flex; align-items: center;font-weight: 600;">
<i class="bi bi-box-arrow-right"></i> Sign Out
</button>
</li>
</ul>
</li>
</ul>
</nav>
<script type="module" th:src="@{/js/pages/fragments/header.js}" defer></script>
</header>
</html>