Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -23,10 +22,6 @@ public class CaseStatsController {
public ResponseEntity<?> getOverview(HttpSession session) {
// 결과가 없으면 404, 있으면 200
CaseStatsOverviewResponse response = caseStatsService.getOverview(session);
if (response == null) {
return ResponseEntity.status(404)
.body(Collections.singletonMap("message", "개요 정보가 없습니다."));
}
return ResponseEntity.ok(response);
}

Expand All @@ -36,10 +31,6 @@ public ResponseEntity<?> getHourlyCaseStats(@RequestParam("date") String date,
@RequestParam(value = "category", required = false) String category,
HttpSession session) {
List<HourlyCaseStatsResponse> stats = caseStatsService.getHourlyCaseStats(date, category, session);
if (stats.isEmpty()) {
return ResponseEntity.status(404)
.body(Collections.singletonMap("message", "시간대별 사건 정보가 없습니다."));
}
return ResponseEntity.ok(stats);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public CaseStatsOverviewResponse getOverview(HttpSession session) {

// 최근 한 달간 데이터를 기준으로 사건 건수가 가장 많은 CCTV 주소 조회
String patrolRegionAddress = statsOverviewRepository.findAddressWithMostIncidentsLastMonth(officeId)
.orElse("정보 없음");
.orElse("해당 지역에 순찰 강화 필요 구역이 없습니다.");

return CaseStatsOverviewResponse.fromEntity(stats, patrolRegionAddress);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package com.example.backend.common;

import org.springframework.web.bind.annotation.ControllerAdvice;

import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.io.IOException;
import java.util.Collections;
import java.util.NoSuchElementException;

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

// SSE 연결 종료 (브라우저에서 탭 닫거나, 연결 끊긴 경우)
@ExceptionHandler(IOException.class)
public void handleIOException(IOException e) {
log.warn("SSE 연결 종료: {}", e.getMessage());
}

@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<?> handleIllegalStateException(IllegalStateException e) {
if (e.getMessage().contains("로그인이 필요합니다.")) {
log.warn("IllegalStateException: {}", e.getMessage());
if (e.getMessage().contains("로그인이 필요")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Collections.singletonMap("message", e.getMessage()));
}
Expand All @@ -36,8 +47,19 @@ public ResponseEntity<?> handleEntityNotFoundException(EntityNotFoundException e
}

@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleGenericException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Collections.singletonMap("message", e.getMessage()));
public void handleGenericException(Exception e, HttpServletResponse response) throws IOException {
log.error("Unexpected error: ", e);
if ("text/event-stream".equals(response.getContentType())) {
// SSE 통신 도중 에러 → event-stream 포맷으로 전달
response.getWriter().write("event: error\n");
response.getWriter().write("data: " + e.getMessage() + "\n\n");
response.getWriter().flush();
} else {
// 일반 요청 → JSON 에러 응답
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"message\": \"" + e.getMessage() + "\"}");
response.getWriter().flush();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public void addCorsMappings(CorsRegistry corsRegistry) {
corsRegistry.addMapping("/**")
.allowedOrigins(allowedOrigins)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD")
.allowedHeaders("Authorization", "Content-Type")
.exposedHeaders("Authorization")
.allowedHeaders("Authorization", "Content-Type", "Last-Event-ID")
.exposedHeaders("Authorization", "Content-Type", "Cache-Control", "Last-Event-ID")
.allowCredentials(true);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package com.example.backend.config;

import java.util.Arrays;
import java.util.List;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
Expand All @@ -15,6 +12,9 @@
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
Expand All @@ -28,22 +28,21 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
return httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
// CORS 설정을 수동으로 추가
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.build();
}

// CORS 설정을 위한 Bean 정의
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(allowedOrigins)); // 프론트엔드 도메인
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD")); // 허용할 HTTP 메서드
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); // 허용할 헤더
configuration.setExposedHeaders(List.of("Authorization")); // 응답에서 노출할 헤더
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Last-Event-ID")); // 허용할 헤더
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type", "Cache-Control", "Last-Event-ID")); // 응답에서 노출할 헤더
configuration.setAllowCredentials(true); // 자격 증명 포함 요청 허용

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 모든 경로에 대해 CORS 설정 적용
return source;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.example.backend.dashboard.dto.CaseDetectResponse;
import com.example.backend.dashboard.service.CaseDetectService;
import com.example.backend.dashboard.service.SseEmitterService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
Expand All @@ -17,6 +19,9 @@ public class CaseDetectController {
private final CaseDetectService caseDetectService;
private final SseEmitterService sseEmitterService;

@Value("${cors.allowed-origins}")
private String allowedOrigin;

@PostMapping("/detect")
public ResponseEntity<?> detectAlarm(@RequestBody CaseDetectRequest request) {
CaseDetectResponse response = caseDetectService.saveCase(request);
Expand All @@ -25,7 +30,12 @@ public ResponseEntity<?> detectAlarm(@RequestBody CaseDetectRequest request) {
}

@GetMapping("/subscribe")
public SseEmitter subscribe() {
public SseEmitter subscribe(HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", allowedOrigin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");

return sseEmitterService.createEmitter();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -22,10 +21,6 @@ public class DashboardController {
@GetMapping("")
public ResponseEntity<?> getCases(HttpSession session) {
List<DashboardResponse> cases = dashboardService.getCases(session);
if (cases.isEmpty()) {
return ResponseEntity.status(404)
.body(Collections.singletonMap("message", "사건이 없습니다."));
}
return ResponseEntity.ok(cases);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ public List<DashboardResponse> getCases(HttpSession session) {
);
List<CaseEntity> cases = dashboardRepository.findAllByOfficeIdAndStateInOrderById(officeId, targetStates);

if (cases.isEmpty()) {
throw new NoSuchElementException("미확인, 확인 또는 출동 중인 사건이 없습니다.");
}

return cases.stream()
.map(DashboardResponse::fromEntity)
.collect(Collectors.toList());
Expand All @@ -80,6 +84,11 @@ public Map<String, String> getCaseVideo(int id, HttpSession session) {
throw new EntityNotFoundException("해당 사건에 대한 영상이 없습니다.");
}

if (caseEntity.getState() == CaseEntity.CaseState.미확인) {
caseEntity.setState(CaseEntity.CaseState.확인);
dashboardRepository.save(caseEntity);
}

return Collections.singletonMap("video", videoUrl);
}

Expand Down
Loading