Skip to content

Commit b4c1d07

Browse files
authored
Merge pull request #75 from Geumpumta/feat/maintenance
feat : 서버 점검 상태 확인 API 및 필터를 통해 점검 시 API 호출 차단 로직 추가
2 parents 927eb5b + 6b3e190 commit b4c1d07

File tree

13 files changed

+484
-2
lines changed

13 files changed

+484
-2
lines changed

src/main/java/com/gpt/geumpumtabackend/global/config/security/SecurityConfig.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
import com.gpt.geumpumtabackend.global.jwt.JwtAuthenticationFilter;
6+
import com.gpt.geumpumtabackend.global.maintenance.MaintenanceFilter;
67
import com.gpt.geumpumtabackend.global.oauth.handler.OAuth2AuthenticationFailureHandler;
78
import com.gpt.geumpumtabackend.global.oauth.handler.OAuth2AuthenticationSuccessHandler;
89
import com.gpt.geumpumtabackend.global.oauth.resolver.CustomAuthorizationRequestResolver;
@@ -36,6 +37,7 @@ public class SecurityConfig {
3637
private final CustomAuthorizationRequestResolver customAuthorizationRequestResolver;
3738
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient;
3839
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
40+
private final MaintenanceFilter maintenanceFilter;
3941

4042
@Bean
4143
public SecurityFilterChain filterChainPermitAll(HttpSecurity http) throws Exception {
@@ -62,6 +64,7 @@ public HttpSecurity defaultSecurity(HttpSecurity http) throws Exception {
6264
.successHandler(oAuth2AuthenticationSuccessHandler)
6365
.failureHandler(oAuth2AuthenticationFailureHandler)
6466
)
67+
.addFilterBefore(maintenanceFilter, UsernamePasswordAuthenticationFilter.class)
6568
.addFilterAfter(new JwtAuthenticationFilter(authenticationManager),
6669
UsernamePasswordAuthenticationFilter.class);
6770
}
@@ -82,4 +85,4 @@ public MethodSecurityExpressionHandler expressionHandler(RoleHierarchy roleHiera
8285
return expressionHandler;
8386
}
8487

85-
}
88+
}

src/main/java/com/gpt/geumpumtabackend/global/exception/ExceptionType.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ public enum ExceptionType {
7373
BADGE_NOT_FOUND(NOT_FOUND, "B001", "배지가 존재하지 않습니다"),
7474
BADGE_NOT_OWNED(FORBIDDEN, "B002", "해당 배지를 소유하지 않습니다"),
7575
BADGE_CODE_ALREADY_EXISTS(CONFLICT, "B003", "이미 존재하는 배지 코드입니다"),
76-
BADGE_IN_USE(CONFLICT, "B004", "이미 지급되어 삭제할 수 없는 배지입니다")
76+
BADGE_IN_USE(CONFLICT, "B004", "이미 지급되어 삭제할 수 없는 배지입니다"),
77+
78+
// Maintenance
79+
MAINTENANCE_IN_PROGRESS(SERVICE_UNAVAILABLE, "MT001", "서버 점검 중입니다.")
7780
;
7881

7982
private final HttpStatus status;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.gpt.geumpumtabackend.global.maintenance;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.gpt.geumpumtabackend.global.exception.ExceptionType;
5+
import com.gpt.geumpumtabackend.global.response.ResponseUtil;
6+
import com.gpt.geumpumtabackend.maintenance.service.MaintenanceService;
7+
import jakarta.servlet.FilterChain;
8+
import jakarta.servlet.ServletException;
9+
import jakarta.servlet.http.HttpServletRequest;
10+
import jakarta.servlet.http.HttpServletResponse;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.http.MediaType;
13+
import org.springframework.stereotype.Component;
14+
import org.springframework.util.AntPathMatcher;
15+
import org.springframework.web.filter.OncePerRequestFilter;
16+
17+
import java.io.IOException;
18+
import java.util.List;
19+
20+
@Component
21+
@RequiredArgsConstructor
22+
public class MaintenanceFilter extends OncePerRequestFilter {
23+
24+
private static final List<String> WHITELIST = List.of(
25+
"/api/v1/maintenance/status",
26+
"/actuator/health",
27+
"/swagger-ui",
28+
"/swagger-ui/",
29+
"/swagger-ui/**",
30+
"/v3/api-docs",
31+
"/v3/api-docs/**",
32+
"/error"
33+
);
34+
35+
private final MaintenanceService maintenanceService;
36+
private final ObjectMapper objectMapper;
37+
private final AntPathMatcher pathMatcher = new AntPathMatcher();
38+
39+
@Override
40+
protected void doFilterInternal(
41+
HttpServletRequest request,
42+
HttpServletResponse response,
43+
FilterChain filterChain
44+
) throws ServletException, IOException {
45+
String requestUri = request.getServletPath();
46+
47+
if (isWhitelisted(requestUri) || !maintenanceService.isMaintenanceInProgress()) {
48+
filterChain.doFilter(request, response);
49+
return;
50+
}
51+
52+
response.setStatus(ExceptionType.MAINTENANCE_IN_PROGRESS.getStatus().value());
53+
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
54+
response.setCharacterEncoding("UTF-8");
55+
objectMapper.writeValue(
56+
response.getWriter(),
57+
ResponseUtil.createFailureResponse(ExceptionType.MAINTENANCE_IN_PROGRESS)
58+
);
59+
}
60+
61+
private boolean isWhitelisted(String requestUri) {
62+
return WHITELIST.stream().anyMatch(pattern -> pathMatcher.match(pattern, requestUri));
63+
}
64+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.gpt.geumpumtabackend.maintenance.api;
2+
3+
import com.gpt.geumpumtabackend.global.config.swagger.SwaggerApiFailedResponse;
4+
import com.gpt.geumpumtabackend.global.config.swagger.SwaggerApiResponses;
5+
import com.gpt.geumpumtabackend.global.config.swagger.SwaggerApiSuccessResponse;
6+
import com.gpt.geumpumtabackend.global.exception.ExceptionType;
7+
import com.gpt.geumpumtabackend.global.response.ResponseBody;
8+
import com.gpt.geumpumtabackend.maintenance.dto.request.MaintenanceStatusUpdateRequest;
9+
import com.gpt.geumpumtabackend.maintenance.dto.response.MaintenanceStatusResponse;
10+
import io.swagger.v3.oas.annotations.Operation;
11+
import io.swagger.v3.oas.annotations.media.Content;
12+
import io.swagger.v3.oas.annotations.media.Schema;
13+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
14+
import io.swagger.v3.oas.annotations.tags.Tag;
15+
import jakarta.validation.Valid;
16+
import org.springframework.http.ResponseEntity;
17+
import org.springframework.security.access.prepost.PreAuthorize;
18+
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.PatchMapping;
20+
import org.springframework.web.bind.annotation.RequestBody;
21+
22+
@Tag(name = "서비스 상태 API", description = "서비스 점검 상태 조회 및 변경 API")
23+
public interface MaintenanceApi {
24+
25+
@Operation(
26+
summary = "점검 상태 조회",
27+
description = "현재 서비스 점검 상태와 안내 메시지를 조회합니다."
28+
)
29+
@ApiResponse(content = @Content(schema = @Schema(implementation = MaintenanceStatusResponse.class)))
30+
@SwaggerApiResponses(
31+
success = @SwaggerApiSuccessResponse(
32+
response = MaintenanceStatusResponse.class,
33+
description = "점검 상태 조회 성공"
34+
)
35+
)
36+
@GetMapping("/status")
37+
ResponseEntity<ResponseBody<MaintenanceStatusResponse>> getCurrentStatus();
38+
39+
@Operation(
40+
summary = "점검 상태 변경",
41+
description = """
42+
ADMIN 권한으로 서비스 점검 상태를 변경합니다.
43+
- status: NORMAL 또는 MAINTENANCE
44+
- message: 점검 안내 문구 (선택)
45+
"""
46+
)
47+
@ApiResponse(content = @Content(schema = @Schema(implementation = MaintenanceStatusResponse.class)))
48+
@SwaggerApiResponses(
49+
success = @SwaggerApiSuccessResponse(
50+
response = MaintenanceStatusResponse.class,
51+
description = "점검 상태 변경 성공"
52+
),
53+
errors = {
54+
@SwaggerApiFailedResponse(ExceptionType.NEED_AUTHORIZED),
55+
@SwaggerApiFailedResponse(ExceptionType.ACCESS_DENIED)
56+
}
57+
)
58+
@PatchMapping("/status")
59+
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
60+
ResponseEntity<ResponseBody<MaintenanceStatusResponse>> updateStatus(
61+
@RequestBody @Valid MaintenanceStatusUpdateRequest request
62+
);
63+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.gpt.geumpumtabackend.maintenance.controller;
2+
3+
import com.gpt.geumpumtabackend.global.response.ResponseBody;
4+
import com.gpt.geumpumtabackend.global.response.ResponseUtil;
5+
import com.gpt.geumpumtabackend.maintenance.api.MaintenanceApi;
6+
import com.gpt.geumpumtabackend.maintenance.dto.request.MaintenanceStatusUpdateRequest;
7+
import com.gpt.geumpumtabackend.maintenance.dto.response.MaintenanceStatusResponse;
8+
import com.gpt.geumpumtabackend.maintenance.service.MaintenanceService;
9+
import jakarta.validation.Valid;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.access.prepost.PreAuthorize;
13+
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.PatchMapping;
15+
import org.springframework.web.bind.annotation.RequestBody;
16+
import org.springframework.web.bind.annotation.RequestMapping;
17+
import org.springframework.web.bind.annotation.RestController;
18+
19+
@RestController
20+
@RequiredArgsConstructor
21+
@RequestMapping("/api/v1/maintenance")
22+
public class MaintenanceController implements MaintenanceApi {
23+
24+
private final MaintenanceService maintenanceService;
25+
26+
@GetMapping("/status")
27+
public ResponseEntity<ResponseBody<MaintenanceStatusResponse>> getCurrentStatus() {
28+
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(
29+
maintenanceService.getCurrentStatus()
30+
));
31+
}
32+
33+
@PatchMapping("/status")
34+
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
35+
public ResponseEntity<ResponseBody<MaintenanceStatusResponse>> updateStatus(
36+
@RequestBody @Valid MaintenanceStatusUpdateRequest request
37+
) {
38+
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(
39+
maintenanceService.updateStatus(request)
40+
));
41+
}
42+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.gpt.geumpumtabackend.maintenance.domain;
2+
3+
import com.gpt.geumpumtabackend.global.base.BaseEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.EnumType;
7+
import jakarta.persistence.Enumerated;
8+
import jakarta.persistence.Id;
9+
import lombok.AccessLevel;
10+
import lombok.Getter;
11+
import lombok.NoArgsConstructor;
12+
13+
@Entity
14+
@Getter
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
public class Maintenance extends BaseEntity {
17+
18+
public static final Long DEFAULT_ID = 1L;
19+
20+
@Id
21+
private Long id;
22+
23+
@Enumerated(EnumType.STRING)
24+
@Column(nullable = false)
25+
private ServiceStatus status;
26+
27+
@Column(length = 255)
28+
private String message;
29+
30+
private Maintenance(Long id, ServiceStatus status, String message) {
31+
this.id = id;
32+
this.status = status;
33+
this.message = message;
34+
}
35+
36+
public static Maintenance initialize(ServiceStatus status, String message) {
37+
return new Maintenance(DEFAULT_ID, status, message);
38+
}
39+
40+
public void update(ServiceStatus status, String message) {
41+
this.status = status;
42+
this.message = message;
43+
}
44+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.gpt.geumpumtabackend.maintenance.domain;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum ServiceStatus {
7+
NORMAL("정상"),
8+
MAINTENANCE("점검중");
9+
10+
private final String status;
11+
12+
ServiceStatus(String status) {
13+
this.status = status;
14+
}
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.gpt.geumpumtabackend.maintenance.dto.request;
2+
3+
import com.gpt.geumpumtabackend.maintenance.domain.ServiceStatus;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
public record MaintenanceStatusUpdateRequest(
7+
@NotNull ServiceStatus status,
8+
String message
9+
) {
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.gpt.geumpumtabackend.maintenance.dto.response;
2+
3+
import com.gpt.geumpumtabackend.maintenance.domain.Maintenance;
4+
import com.gpt.geumpumtabackend.maintenance.domain.ServiceStatus;
5+
6+
public record MaintenanceStatusResponse(
7+
ServiceStatus status,
8+
String message
9+
) {
10+
public static MaintenanceStatusResponse from(Maintenance maintenance) {
11+
return new MaintenanceStatusResponse(
12+
maintenance.getStatus(),
13+
maintenance.getMessage()
14+
);
15+
}
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.gpt.geumpumtabackend.maintenance.repository;
2+
3+
import com.gpt.geumpumtabackend.maintenance.domain.Maintenance;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface MaintenanceRepository extends JpaRepository<Maintenance, Long> {
7+
}

0 commit comments

Comments
 (0)