[Feat] 훈련 현황 실시간 모니터링 WebSocket 구현 - #33
Conversation
|
Warning Review limit reached
Next review available in: 10 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughJWT 기반 HTTP·WebSocket 인증과 역할 제어를 추가했습니다. 훈련 상태 WebSocket 이벤트 발행을 구현했습니다. 로그인 응답에 토큰 정보를 포함했습니다. 테스트와 develop 브랜치용 Docker·EC2 CD 워크플로를 추가했습니다. Changes보안 및 로그인
STOMP WebSocket 모니터링
배포 자동화
Estimated code review effort: 4 (Complex) | ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🔵 TrivialWebSocket 세션은 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 winissuer 불일치·만료 토큰 거부 경로에 대한 테스트가 없습니다.
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
📒 Files selected for processing (30)
.github/workflows/cd.yamldocker-compose.prod.ymldocker-compose.ymlsrc/main/java/com/saferoute/domain/user/dto/LoginResponse.javasrc/main/java/com/saferoute/domain/user/service/UserService.javasrc/main/java/com/saferoute/global/config/OpenApiConfig.javasrc/main/java/com/saferoute/global/config/SecurityConfig.javasrc/main/java/com/saferoute/global/security/CustomUserDetailsService.javasrc/main/java/com/saferoute/global/security/JwtAccessDeniedHandler.javasrc/main/java/com/saferoute/global/security/JwtAuthenticationEntryPoint.javasrc/main/java/com/saferoute/global/security/JwtAuthenticationFilter.javasrc/main/java/com/saferoute/global/security/JwtProperties.javasrc/main/java/com/saferoute/global/security/JwtTokenProvider.javasrc/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventMessage.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventType.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/TrainingStatusEventData.javasrc/main/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptor.javasrc/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.javasrc/main/resources/application-prod.ymlsrc/main/resources/application.ymlsrc/test/java/com/saferoute/domain/evacuation/controller/EvacuationRouteControllerTest.javasrc/test/java/com/saferoute/domain/evacuation/controller/MapGraphControllerTest.javasrc/test/java/com/saferoute/domain/evacuation/controller/MapGraphEditControllerTest.javasrc/test/java/com/saferoute/global/security/JwtTokenProviderTest.javasrc/test/java/com/saferoute/global/security/SecurityAuthorizationIntegrationTest.javasrc/test/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptorTest.javasrc/test/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisherTest.javasrc/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.javasrc/test/resources/application.yml
| - 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" |
There was a problem hiding this comment.
🎯 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.
There was a problem hiding this comment.
@songmin0111, 알겠습니다. 이 PR에서는 CD 관련 수정 요청을 보류하겠습니다.
나중에 CD를 진행할 때는 docker-compose.prod.yml이 배포한 이미지 태그를 참조하도록 구성해야 합니다. 원하시면 후속 GitHub 이슈를 생성하겠습니다.
(ᵕ•ᴗ•)🐇
You are interacting with an AI system.
관련 이슈
Closes #31
작업 배경
관리자 웹은 현재 훈련 세션 상태를 REST GET 폴링으로만 확인할 수 있다.
훈련 중 발생하는 상태 변화(진행 상태, 향후 혼잡도·경로·유도등 상태)를
관리자 대시보드에 실시간으로 전달하기 위해 STOMP 기반 WebSocket을 도입한다.
브랜치 의존성
이 PR은 아직
dev에 머지되지 않은feat/#29JWT 구현을 기반으로 한다.feat/#29가dev에 머지된 후 rebase하여dev대상 PR로 전환해야 한다.주요 구현 내용
@EnableWebSocketMessageBroker기반 STOMP 설정 (WebSocketConfig)StompAuthChannelInterceptor)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 인증 방식
Authorization: Bearer {token}헤더를 기존JwtTokenProvider.getEmail()+CustomUserDetailsService로 검증accessor.setUser(...)로 Principal을 STOMP 세션에 저장, 이후 SUBSCRIBE 프레임에서 재확인/topic/training-sessions/{UUID}) 검증 +TrainingSessionRepository.existsById()로 실제 존재하는 세션인지 확인User-TrainingSession간 소유 관계가 도메인에 없어 "본인이 담당하는 세션만 구독 가능"한 제한은 구현하지 않았다. 로그인한 모든 MANAGER가 모든 세션 topic을 구독할 수 있다.AuthenticationCredentialsNotFoundException,BadCredentialsException,AccessDeniedException)를 던져 STOMP ERROR 프레임 후 연결 종료. 토큰 원문·이메일은 로그에 남기지 않음.REST와 WebSocket 역할 구분
테스트 코드 및 실행 결과
StompAuthChannelInterceptorTest(단위, Mockito) — CONNECT/SUBSCRIBE 인증·인가 13케이스TrainingEventPublisherTest(단위, Mockito) — destination/envelope 필드 무결성, 트랜잭션 커밋 전후 발행 시점WebSocketIntegrationTest(RANDOM_PORT,WebSocketStompClient) — MANAGER 연결·구독·이벤트 수신, 인증 실패, NORMAL 권한 거부로컬 수동 테스트 방법
./gradlew bootRun(local 프로파일,docker-compose.yml로 Postgres 먼저 기동)POST /api/v1/auth/login호출해accessToken획득@stomp/stompjs로 연결:onStompError호출 또는 즉시 연결 종료로 인증 실패 확인WebSocketIntegrationTest처럼TrainingEventPublisher.publishTrainingStatusUpdated(session)를 코드에서 직접 호출해야 수신 확인 가능 (아래 참고)프론트엔드 연동 가이드
brokerURL방식 그대로 사용 (SockJS 아님)connectHeaders.Authorization에Bearer {accessToken}필수 — 없으면 연결 자체가 거부됨/topic/training-sessions/{sessionId}고정 (UUID 형식 아니면 SUBSCRIBE 거부)후속 연동이 필요한 이벤트 / 팀원 작업 필요 항목
Summary by CodeRabbit
새 기능
배포
테스트