Skip to content

[Feat] 훈련 현황 실시간 모니터링 WebSocket 구현 - #33

Merged
songmin0111 merged 14 commits into
developfrom
feat/#31
Aug 3, 2026
Merged

songmin0111 merged 14 commits into
developfrom
feat/#31

Conversation

@songmin0111

@songmin0111 songmin0111 commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

관련 이슈

Closes #31

작업 배경

관리자 웹은 현재 훈련 세션 상태를 REST GET 폴링으로만 확인할 수 있다.
훈련 중 발생하는 상태 변화(진행 상태, 향후 혼잡도·경로·유도등 상태)를
관리자 대시보드에 실시간으로 전달하기 위해 STOMP 기반 WebSocket을 도입한다.

브랜치 의존성

이 PR은 아직 dev에 머지되지 않은 feat/#29 JWT 구현을 기반으로 한다.
feat/#29가 dev에 머지된 후 rebase하여 dev 대상 PR로 전환해야 한다.

주요 구현 내용

  • @EnableWebSocketMessageBroker 기반 STOMP 설정 (WebSocketConfig)
  • STOMP CONNECT/SUBSCRIBE 단계 JWT 인증·인가 인터셉터 (StompAuthChannelInterceptor)
  • 훈련 상태 이벤트 공통 envelope + 발행 서비스 (TrainingEventMessage, TrainingEventType, TrainingStatusEventData, TrainingEventPublisher)
  • SecurityConfig에 /ws/** handshake 경로 permitAll 추가 (실제 인증은 STOMP 레이어에서 수행)

WebSocket endpoint

  • /ws (STOMP, SockJS 미사용)

구독 destination

  • /topic/training-sessions/{sessionId}
  • 사용자별 오류 큐: /user/queue/errors (검증 로직만 존재, 아직 발행 지점 없음)

이벤트 메시지 규격

{
      "eventType": "TRAINING_STATUS_UPDATED",
      "sessionId": "UUID",
      "occurredAt": "2026-08-03T00:00:00Z",
      "data": {
        "status": "COMPLETED",
        "startedAt": "2026-08-03T00:00:00Z",
        "endedAt": "2026-08-03T00:30:00Z"
  }
}

TrainingEventType에는 CONGESTION_UPDATED, EVACUATION_ROUTE_UPDATED, IOT_LIGHT_STATUS_UPDATED도 계약용으로 미리 정의해 두었으나, 대응 데이터 DTO·발행 메서드는 이번 PR에 포함하지 않았다 (아래 "후속 연동이 필요한 이벤트" 참고).

JWT/ADMIN 인증 방식

  • STOMP CONNECT 프레임의 Authorization: Bearer {token} 헤더를 기존 JwtTokenProvider.getEmail() + CustomUserDetailsService로 검증
  • 인증 성공 시 accessor.setUser(...)로 Principal을 STOMP 세션에 저장, 이후 SUBSCRIBE 프레임에서 재확인
  • SUBSCRIBE 시 destination 형식(/topic/training-sessions/{UUID}) 검증 + TrainingSessionRepository.existsById()로 실제 존재하는 세션인지 확인
  • 알려진 제한: User-TrainingSession 간 소유 관계가 도메인에 없어 "본인이 담당하는 세션만 구독 가능"한 제한은 구현하지 않았다. 로그인한 모든 MANAGER가 모든 세션 topic을 구독할 수 있다.
  • 인증/인가 실패 시 Spring Security 표준 예외(AuthenticationCredentialsNotFoundException, BadCredentialsException, AccessDeniedException)를 던져 STOMP ERROR 프레임 후 연결 종료. 토큰 원문·이메일은 로그에 남기지 않음.

REST와 WebSocket 역할 구분

  • REST: 훈련 생성·조회 등 관리자 웹 → 서버 명령 (기존 그대로 유지, 변경 없음)
  • WebSocket: 서버 → 관리자 대시보드 단방향 상태 브로드캐스트 전용
  • 라즈베리파이 → 서버 텔레메트리: 기존 REST 유지

테스트 코드 및 실행 결과

  • StompAuthChannelInterceptorTest (단위, Mockito) — CONNECT/SUBSCRIBE 인증·인가 13케이스
  • TrainingEventPublisherTest (단위, Mockito) — destination/envelope 필드 무결성, 트랜잭션 커밋 전후 발행 시점
  • WebSocketIntegrationTest (RANDOM_PORT, WebSocketStompClient) — MANAGER 연결·구독·이벤트 수신, 인증 실패, NORMAL 권한 거부

로컬 수동 테스트 방법

  1. ./gradlew bootRun (local 프로파일, docker-compose.yml로 Postgres 먼저 기동)
  2. MANAGER 계정으로 POST /api/v1/auth/login 호출해 accessToken 획득
  3. 브라우저/Node에서 @stomp/stompjs로 연결:
   import { Client } from "@stomp/stompjs";

   const client = new Client({
     brokerURL: "ws://localhost:8080/ws",
     connectHeaders: {
       Authorization: `Bearer ${accessToken}`
     },
     onConnect: () => {
       client.subscribe(
         `/topic/training-sessions/${sessionId}`,
         message => console.log(JSON.parse(message.body))
       );
     },
     onStompError: frame => console.error("STOMP 오류:", frame)
   });

   client.activate();
  1. Authorization 헤더 없이 연결 시도 → onStompError 호출 또는 즉시 연결 종료로 인증 실패 확인
  2. NORMAL 계정 토큰으로 반복 → 동일하게 거부 확인
  3. 실제 이벤트 트리거 방법이 아직 없음 — WebSocketIntegrationTest처럼 TrainingEventPublisher.publishTrainingStatusUpdated(session)를 코드에서 직접 호출해야 수신 확인 가능 (아래 참고)

프론트엔드 연동 가이드

  • brokerURL 방식 그대로 사용 (SockJS 아님)
  • connectHeaders.Authorization에 Bearer {accessToken} 필수 — 없으면 연결 자체가 거부됨
  • 구독 경로는 /topic/training-sessions/{sessionId} 고정 (UUID 형식 아니면 SUBSCRIBE 거부)
  • 수신 메시지는 위 "이벤트 메시지 규격" JSON 그대로

후속 연동이 필요한 이벤트 / 팀원 작업 필요 항목

항목 필요한 작업 비고
TRAINING_STATUS_UPDATED 실제 발행 훈련 시작/종료 API
CONGESTION_UPDATED 혼잡도 계산 도메인 로직
EVACUATION_ROUTE_UPDATED 경로 재계산 트리거
IOT_LIGHT_STATUS_UPDATED 실시간 방향 상태 저장 방식

Summary by CodeRabbit

  • 새 기능

    • 로그인 응답에 JWT 액세스 토큰과 만료 정보가 포함됩니다.
    • JWT 기반 인증 및 역할별 API 접근 제어가 적용됩니다.
    • 관리자 대시보드에서 훈련 상태 이벤트를 실시간 WebSocket으로 받을 수 있습니다.
    • API 문서에서 Bearer 인증을 사용할 수 있습니다.
  • 배포

    • 자동 배포 및 배포 후 상태 확인 절차가 추가되었습니다.
    • 운영 환경의 JWT 및 AWS 설정이 개선되었습니다.
  • 테스트

    • 인증, 권한, WebSocket, 토큰 검증 관련 통합 테스트가 강화되었습니다.

@songmin0111 songmin0111 linked an issue Aug 2, 2026 that may be closed by this pull request
4 tasks
@songmin0111 songmin0111 self-assigned this Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@songmin0111, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 28fb4dd1-2b8d-4fde-9212-8426ad440b8a

📥 Commits

Reviewing files that changed from the base of the PR and between 01790b9 and fdcd27d.

📒 Files selected for processing (7)
  • src/main/java/com/saferoute/global/config/SecurityConfig.java
  • src/main/java/com/saferoute/global/security/JwtAuthenticationFilter.java
  • src/main/java/com/saferoute/global/security/JwtTokenProvider.java
  • src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java
  • src/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.java
  • src/main/resources/application.yml
  • src/test/java/com/saferoute/domain/device/controller/IoTLightControllerTest.java
📝 Walkthrough

Walkthrough

JWT 기반 HTTP·WebSocket 인증과 역할 제어를 추가했습니다. 훈련 상태 WebSocket 이벤트 발행을 구현했습니다. 로그인 응답에 토큰 정보를 포함했습니다. 테스트와 develop 브랜치용 Docker·EC2 CD 워크플로를 추가했습니다.

Changes

보안 및 로그인

Layer / File(s) Summary
JWT 계약과 로그인 토큰 발급
src/main/java/com/saferoute/global/security/*, src/main/java/com/saferoute/domain/user/*, src/main/resources/application.yml, docker-compose*.yml, src/test/java/com/saferoute/global/security/JwtTokenProviderTest.java
JWT 설정과 토큰 생성·검증을 추가했습니다. 로그인 응답에 Bearer, access token, 만료 시간을 포함했습니다.
HTTP JWT 인증과 권한 제어
src/main/java/com/saferoute/global/config/*, src/main/java/com/saferoute/global/security/*, src/test/java/com/saferoute/global/security/SecurityAuthorizationIntegrationTest.java, src/test/java/com/saferoute/domain/evacuation/controller/*
무상태 보안 필터 체인과 역할별 API 접근 제어를 구성했습니다. 인증 오류와 접근 거부 응답을 JSON으로 반환합니다. 기존 컨트롤러 테스트를 인증 컨텍스트 기반 통합 테스트로 변경했습니다.

STOMP WebSocket 모니터링

Layer / File(s) Summary
STOMP 인증과 훈련 이벤트 발행
src/main/java/com/saferoute/infrastructure/websocket/*, src/test/java/com/saferoute/infrastructure/websocket/*
/ws STOMP 엔드포인트와 MANAGER 전용 CONNECT·SUBSCRIBE 검증을 추가했습니다. 세션별 훈련 상태 이벤트를 즉시 또는 커밋 후 발행합니다. 단위 테스트와 통합 테스트가 인증, 구독, 이벤트 수신, 롤백 동작을 검증합니다.

배포 자동화

Layer / File(s) Summary
테스트·이미지 빌드·EC2 배포
.github/workflows/cd.yaml, docker-compose.prod.yml, src/main/resources/application-prod.yml
develop push 또는 수동 실행 시 테스트, Docker 이미지 푸시, EC2 Compose 재시작, 헬스 체크를 수행합니다. 실패 시 애플리케이션 로그를 출력합니다. AWS 설정과 production JWT 환경 변수를 갱신했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

당근 든 토끼가 토큰을 발급해요
웹소켓 길에 훈련 소식이 달려요
매니저만 문을 열고 들어가요
커밋 뒤에 이벤트가 도착해요
CD 열차가 EC2로 출발해요

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning [#31]과 직접 관련 없는 CD 워크플로, Docker Compose 설정, AWS 설정 변경이 포함되어 범위를 벗어납니다. CD, 배포 환경 변수, AWS 설정 변경을 별도 이슈 또는 PR로 분리하고, 이 PR에는 WebSocket과 필요한 인증 변경만 남기십시오.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 관리자 대시보드의 훈련 현황 실시간 모니터링을 위한 WebSocket 구현이라는 주요 변경을 정확히 설명합니다.
Linked Issues check ✅ Passed 변경 사항은 [#31]의 STOMP 설정, MANAGER JWT 인증, 세션별 구독, 이벤트 envelope·Publisher 및 관련 테스트 요구사항에 부합합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#31

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@songmin0111 songmin0111 added the feat 기능 개발 label Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java (1)

36-39: 🚀 Performance & Scalability | 🔵 Trivial

인메모리 broker는 인스턴스 간 이벤트를 전달하지 않습니다.

enableSimpleBroker는 단일 애플리케이션 인스턴스 내에서만 동작합니다. 배포 환경에서 인스턴스를 여러 개 운영하면, 한 인스턴스에 연결된 관리자는 다른 인스턴스에서 발행된 이벤트를 받지 못합니다. 향후 수평 확장을 계획한다면 외부 메시지 브로커(RabbitMQ, ActiveMQ 등)로 relay하는 구성을 고려하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java`
around lines 36 - 39, Update configureMessageBroker in WebSocketConfig to use an
external STOMP broker relay instead of enableSimpleBroker, configuring the
selected RabbitMQ or ActiveMQ relay destination and connection settings while
preserving the existing /topic, /queue, and /user destinations. Ensure the
configuration supports event delivery across multiple application instances.
src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java (1)

151-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Thread.sleep(300) 대신 구독 완료를 명시적으로 확인하십시오.

고정된 sleep은 느린 CI 환경에서 간헐적으로 실패할 수 있습니다. STOMP receipt 헤더를 사용해 SUBSCRIBE가 브로커에 등록되었다는 확인을 받은 뒤 발행하면 더 안정적입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java`
around lines 151 - 169, Replace the fixed Thread.sleep(300) in the
WebSocketIntegrationTest subscription flow with explicit STOMP receipt handling:
request a receipt when subscribing, wait for the corresponding receipt
confirmation, then call
trainingEventPublisher.publishTrainingStatusUpdated(trainingSession). Preserve
the existing frame handler and ensure publishing occurs only after subscription
registration is acknowledged.
src/main/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptor.java (1)

69-101: 🔒 Security & Privacy | 🔵 Trivial

WebSocket 세션은 JWT 만료 이후에도 계속 인증된 상태로 유지됩니다.

authenticate는 CONNECT 프레임에서만 JWT를 검증합니다. STOMP 연결이 access token 유효 기간(access-token-expiration)보다 오래 유지되면, 토큰이 만료된 이후에도 해당 연결은 계속 MANAGER 권한으로 구독을 이어갈 수 있습니다. 짧은 연결 수명을 전제로 한다면 문제되지 않지만, 장시간 열어두는 대시보드라면 주기적 재인증이나 heartbeat 기반 토큰 재검증을 고려하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptor.java`
around lines 69 - 101, Update the WebSocket authentication flow around
authenticate to revalidate the JWT after CONNECT, using heartbeat or another
periodic session mechanism, and reject or disconnect the session once the access
token expires. Preserve the existing MANAGER authority check while ensuring
long-lived STOMP connections cannot continue authenticated access beyond the
token’s expiration.
src/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.java (1)

22-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

JwtAuthenticationEntryPoint와 JwtAccessDeniedHandler의 응답 작성 로직이 중복됩니다. 두 클래스 모두 상태 코드 설정, Content-Type/인코딩 설정, ApiResponse.failure(...) 직렬화를 거의 동일하게 구현합니다. 공통 헬퍼로 추출하면 응답 형식 변경 시 한 곳만 수정하면 됩니다.

  • src/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.java#L22-L39: 공통 헬퍼(예: SecurityResponseWriter.write(response, ErrorCode))를 추출해 이 메서드에서 호출하도록 변경합니다.
  • src/main/java/com/saferoute/global/security/JwtAccessDeniedHandler.java#L22-L39: 동일한 공통 헬퍼를 호출하도록 변경합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.java`
around lines 22 - 39, JwtAuthenticationEntryPoint와 JwtAccessDeniedHandler의 중복된
응답 작성 로직을 공통 헬퍼로 추출하세요.
src/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.java의
22-39행에서는 새 SecurityResponseWriter.write(response, ErrorCode) 헬퍼를 호출하도록 변경하고,
src/main/java/com/saferoute/global/security/JwtAccessDeniedHandler.java의
22-39행에서도 동일한 헬퍼를 사용하도록 변경하세요. 헬퍼에서 상태 코드, Content-Type, 인코딩 설정 및
ApiResponse.failure(ErrorCode) 직렬화를 처리하세요.
src/main/java/com/saferoute/global/security/JwtTokenProvider.java (1)

55-62: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

issuer 불일치·만료 토큰 거부 경로에 대한 테스트가 없습니다.

parseClaims는 requireIssuer로 issuer를 검증하고, expiration 클레임으로 만료를 검증합니다. 두 경로 모두 인증 우회를 막는 핵심 로직이지만 JwtTokenProviderTest에는 해당 테스트가 없습니다. issuer가 다른 토큰과 만료된 토큰을 각각 getEmail에 전달했을 때 JwtException이 발생하는지 확인하는 테스트를 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/saferoute/global/security/JwtTokenProvider.java` around
lines 55 - 62, JwtTokenProviderTest에 issuer가 properties.issuer()와 다른 토큰 및
expiration이 지난 토큰을 각각 생성해 getEmail에 전달하는 테스트를 추가하세요. 두 테스트 모두 JwtException 발생을
검증하고, 기존 유효 토큰 테스트와 설정 방식을 재사용해 parseClaims의 requireIssuer·만료 검증 경로를 직접 확인하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/cd.yaml:
- Around line 102-108: Update the production Compose app service to define its
image from the IMAGE_REPOSITORY and IMAGE_TAG variables, while preserving the
existing build configuration as appropriate. In the “Pull image and restart
application” deployment step, pass the current github.sha as IMAGE_TAG instead
of the hardcoded develop value so pull and up use the image published by this
workflow.

In `@src/main/java/com/saferoute/global/config/SecurityConfig.java`:
- Around line 60-80: SecurityConfig의 HttpSecurity 설정에 CORS를 활성화하고,
StompAuthChannelInterceptor의 STOMP CONNECT 권한을 NORMAL 사용자도 허용하도록 조정하세요.
actuator는 /actuator/health만 공개하도록 SecurityConfig에서 명시하고, 관리 엔드포인트 노출 설정은
management.endpoints.web.exposure.include에 필요한 항목만 지정하며 민감한 엔드포인트는 제외하세요.

In `@src/main/java/com/saferoute/global/security/JwtAuthenticationFilter.java`:
- Around line 29-41: JwtAuthenticationFilter에 shouldNotFilter를 추가해 요청 경로가
/ws/**에 해당하면 필터를 건너뛰도록 하세요. OncePerRequestFilter의 경로 제외 동작을 사용해
doFilterInternal보다 먼저 반환되게 하고, 그 외 요청의 기존 JWT 처리 흐름은 유지하세요.

In `@src/main/java/com/saferoute/global/security/JwtTokenProvider.java`:
- Around line 64-77: Update createSigningKey so its exception handling also
catches the weak or invalid key exceptions raised by Keys.hmacShaKeyFor, while
preserving the existing IllegalArgumentException handling for invalid Base64
input. Route all invalid or insufficient JWT_SECRET values through the existing
configuration error message and retain the original exception as the cause.

In
`@src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java`:
- Around line 31-32: Update the WebSocket endpoint configuration in
WebSocketConfig so setAllowedOriginPatterns uses a configurable allowlist of
trusted administrator dashboard origins instead of the wildcard "*". Preserve
the existing "/ws" endpoint while sourcing the allowed origins from the
application’s environment or configuration properties.

In
`@src/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.java`:
- Around line 49-63: Update the TransactionSynchronization registered by
publishTrainingStatusUpdatedAfterCommit so afterCommit() catches
RuntimeException from publishTrainingStatusUpdated(session), preventing the
committed request from failing; separately record or handle the publication
failure.

---

Nitpick comments:
In
`@src/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.java`:
- Around line 22-39: JwtAuthenticationEntryPoint와 JwtAccessDeniedHandler의 중복된 응답
작성 로직을 공통 헬퍼로 추출하세요.
src/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.java의
22-39행에서는 새 SecurityResponseWriter.write(response, ErrorCode) 헬퍼를 호출하도록 변경하고,
src/main/java/com/saferoute/global/security/JwtAccessDeniedHandler.java의
22-39행에서도 동일한 헬퍼를 사용하도록 변경하세요. 헬퍼에서 상태 코드, Content-Type, 인코딩 설정 및
ApiResponse.failure(ErrorCode) 직렬화를 처리하세요.

In `@src/main/java/com/saferoute/global/security/JwtTokenProvider.java`:
- Around line 55-62: JwtTokenProviderTest에 issuer가 properties.issuer()와 다른 토큰 및
expiration이 지난 토큰을 각각 생성해 getEmail에 전달하는 테스트를 추가하세요. 두 테스트 모두 JwtException 발생을
검증하고, 기존 유효 토큰 테스트와 설정 방식을 재사용해 parseClaims의 requireIssuer·만료 검증 경로를 직접 확인하세요.

In
`@src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java`:
- Around line 36-39: Update configureMessageBroker in WebSocketConfig to use an
external STOMP broker relay instead of enableSimpleBroker, configuring the
selected RabbitMQ or ActiveMQ relay destination and connection settings while
preserving the existing /topic, /queue, and /user destinations. Ensure the
configuration supports event delivery across multiple application instances.

In
`@src/main/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptor.java`:
- Around line 69-101: Update the WebSocket authentication flow around
authenticate to revalidate the JWT after CONNECT, using heartbeat or another
periodic session mechanism, and reject or disconnect the session once the access
token expires. Preserve the existing MANAGER authority check while ensuring
long-lived STOMP connections cannot continue authenticated access beyond the
token’s expiration.

In
`@src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java`:
- Around line 151-169: Replace the fixed Thread.sleep(300) in the
WebSocketIntegrationTest subscription flow with explicit STOMP receipt handling:
request a receipt when subscribing, wait for the corresponding receipt
confirmation, then call
trainingEventPublisher.publishTrainingStatusUpdated(trainingSession). Preserve
the existing frame handler and ensure publishing occurs only after subscription
registration is acknowledged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a900679-15d0-4a92-98b8-37007f9fd27a

📥 Commits

Reviewing files that changed from the base of the PR and between e0e8b26 and 01790b9.

📒 Files selected for processing (30)
  • .github/workflows/cd.yaml
  • docker-compose.prod.yml
  • docker-compose.yml
  • src/main/java/com/saferoute/domain/user/dto/LoginResponse.java
  • src/main/java/com/saferoute/domain/user/service/UserService.java
  • src/main/java/com/saferoute/global/config/OpenApiConfig.java
  • src/main/java/com/saferoute/global/config/SecurityConfig.java
  • src/main/java/com/saferoute/global/security/CustomUserDetailsService.java
  • src/main/java/com/saferoute/global/security/JwtAccessDeniedHandler.java
  • src/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.java
  • src/main/java/com/saferoute/global/security/JwtAuthenticationFilter.java
  • src/main/java/com/saferoute/global/security/JwtProperties.java
  • src/main/java/com/saferoute/global/security/JwtTokenProvider.java
  • src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java
  • src/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventMessage.java
  • src/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventType.java
  • src/main/java/com/saferoute/infrastructure/websocket/dto/TrainingStatusEventData.java
  • src/main/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptor.java
  • src/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.java
  • src/main/resources/application-prod.yml
  • src/main/resources/application.yml
  • src/test/java/com/saferoute/domain/evacuation/controller/EvacuationRouteControllerTest.java
  • src/test/java/com/saferoute/domain/evacuation/controller/MapGraphControllerTest.java
  • src/test/java/com/saferoute/domain/evacuation/controller/MapGraphEditControllerTest.java
  • src/test/java/com/saferoute/global/security/JwtTokenProviderTest.java
  • src/test/java/com/saferoute/global/security/SecurityAuthorizationIntegrationTest.java
  • src/test/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptorTest.java
  • src/test/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisherTest.java
  • src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java
  • src/test/resources/application.yml

Comment thread .github/workflows/cd.yaml Outdated
Comment on lines +102 to +108
- name: Pull image and restart application
run: |
ssh -i ~/.ssh/saferoute \
"${{ vars.EC2_USER }}@${{ vars.EC2_HOST }}" \
"cd /home/${{ vars.EC2_USER }}/saferoute && \
IMAGE_TAG=develop docker compose --env-file .env -f docker-compose.prod.yml pull && \
IMAGE_TAG=develop docker compose --env-file .env -f docker-compose.prod.yml up -d --remove-orphans"

@coderabbitai coderabbitai Bot Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Compose가 배포할 레지스트리 이미지를 참조하도록 수정하세요.

Line 107의 IMAGE_TAG는 docker-compose.prod.yml에서 사용되지 않습니다. 해당 파일의 app 서비스는 image 없이 build만 정의합니다. 따라서 Line 107-108은 이번 워크플로가 발행한 이미지를 pull하지 못하고, EC2의 기존 로컬 이미지를 재사용할 수 있습니다.

프로덕션 Compose에 ${IMAGE_REPOSITORY}:${IMAGE_TAG} 형식의 image를 정의하세요. 배포 명령에는 이번 빌드의 ${{ github.sha }} 태그를 전달하세요.

수정 예시
# docker-compose.prod.yml
 services:
   app:
-    build:
-      context: .
-      dockerfile: Dockerfile
+    image: ${IMAGE_REPOSITORY}:${IMAGE_TAG}
- IMAGE_TAG=develop docker compose --env-file .env -f docker-compose.prod.yml pull
- IMAGE_TAG=develop docker compose --env-file .env -f docker-compose.prod.yml up -d --remove-orphans
+ IMAGE_REPOSITORY=${{ vars.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} IMAGE_TAG=${{ github.sha }} docker compose --env-file .env -f docker-compose.prod.yml pull
+ IMAGE_REPOSITORY=${{ vars.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} IMAGE_TAG=${{ github.sha }} docker compose --env-file .env -f docker-compose.prod.yml up -d --remove-orphans
🧰 Tools
🪛 zizmor (1.28.0)

[info] 105-105: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 105-105: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 106-106: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/cd.yaml around lines 102 - 108, Update the production
Compose app service to define its image from the IMAGE_REPOSITORY and IMAGE_TAG
variables, while preserving the existing build configuration as appropriate. In
the “Pull image and restart application” deployment step, pass the current
github.sha as IMAGE_TAG instead of the hardcoded develop value so pull and up
use the image published by this workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CD는 나중에 할거야

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@songmin0111, 알겠습니다. 이 PR에서는 CD 관련 수정 요청을 보류하겠습니다.

나중에 CD를 진행할 때는 docker-compose.prod.yml이 배포한 이미지 태그를 참조하도록 구성해야 합니다. 원하시면 후속 GitHub 이슈를 생성하겠습니다.

(ᵕ•ᴗ•)🐇

You are interacting with an AI system.

Comment thread src/main/java/com/saferoute/global/config/SecurityConfig.java
Comment thread src/main/java/com/saferoute/global/security/JwtTokenProvider.java
Comment thread src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[✨ Feature] 훈련 현황 실시간 모니터링 WebSocket 구현

2 participants