commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import lombok.RequiredArgsConstructor;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/user")
|
||||
public class SignUpApi {
|
||||
public class SignApi {
|
||||
|
||||
private final SignUpService signUpService;
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,11 +20,6 @@ public enum JwtTokenRule {
|
||||
*/
|
||||
JWT_ISSUE_HEADER("Set-Cookie"),
|
||||
|
||||
/**
|
||||
* JWT 토큰 해석 시 사용되는 HTTP 헤더 이름입니다.
|
||||
*/
|
||||
JWT_RESOLVE_HEADER("Cookie"),
|
||||
|
||||
/**
|
||||
* 액세스 토큰의 접두사입니다.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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}));
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user