feat: 혼잡 이벤트 수신 및 자동 재탐색 트리거 구현 - #50
Conversation
CCTV 혼잡 이벤트를 수신해 DynamoDB에 저장하고, HIGH/CRITICAL 수준이면 우회 경로를 재탐색해 승인 대기(PENDING) 상태로 저장한다. - CongestionController/Service: POST /api/v1/congestion-events 수신, 대상 건물에 RUNNING 세션이 없으면 조용히 종료 - RouteRecalculation 엔티티/Service: 재탐색 결과 PENDING 저장, 동일 세션+엣지 중복 트리거 방지, 우회 경로 없으면 로그만 남김 - EvacuationRouteService: excludedEdgeIds 오버로드 추가 (기존 findShortestRoute(floorId, startNodeId) 계약/테스트는 유지) - TrainingEventPublisher: CONGESTION_UPDATED, ROUTE_RECALCULATION_REQUESTED WebSocket 이벤트 발행 추가
📝 WalkthroughWalkthrough혼잡 이벤트 수신 API와 검증 DTO를 추가했다. 실행 중인 세션에 혼잡 요약과 WebSocket 이벤트를 저장한다. HIGH 또는 CRITICAL이면 제외 엣지를 반영한 우회 경로를 계산하고 PENDING 재탐색을 생성한다. Changes혼잡 이벤트 수신 및 처리
우회 경로 계산 및 재탐색 상태
재탐색 오케스트레이션 및 알림
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Sensor
participant CongestionController
participant CongestionEventService
participant RouteRecalculationService
participant EvacuationRouteService
participant TrainingEventPublisher
Sensor->>CongestionController: POST /api/v1/congestion-events
CongestionController->>CongestionEventService: reportCongestion(request)
CongestionEventService->>RouteRecalculationService: trigger(session, edge, level)
RouteRecalculationService->>EvacuationRouteService: findShortestRoute(..., excludedEdgeIds)
RouteRecalculationService->>TrainingEventPublisher: publishRouteRecalculationRequestedAfterCommit(...)
Possibly related PRs
Suggested reviewers: 🚥 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
🤖 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
`@src/main/java/com/saferoute/domain/congestion/dto/request/ReportCongestionRequest.java`:
- Around line 10-14: ReportCongestionRequest에 avgHeadcount와 peakHeadcount의 필수값 및
음수가 아닌 값 제약을 추가하고, avgHeadcount가 peakHeadcount를 초과하지 않으며 windowStart가
windowEnd보다 이전이도록 클래스 수준 검증을 적용하세요. CongestionControllerTest의 해당 요청 검증 테스트에 각 제약
위반이 400 응답을 반환하는 테스트를 추가하세요.
In
`@src/main/java/com/saferoute/domain/congestion/service/CongestionEventService.java`:
- Around line 60-66: Update reportCongestion and the
RouteRecalculationService.trigger flow to consistently serialize or deduplicate
concurrent recalculation requests for the same session, triggerEdge, and PENDING
status. Use a pessimistic JPA lock when loading the relevant
session/recalculation state, or enforce the equivalent state-based uniqueness
check, so HIGH/CRITICAL congestion events cannot both be discarded before
recalculation starts.
In
`@src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java`:
- Around line 66-84: Update RouteRecalculationService.trigger() so the
ROUTE_RECALCULATED TrainingEvent is recorded as a JPA outbox entry within the
same transaction as routeRecalculationRepository.save(...), rather than calling
trainingEventRepository.save(...) directly. Add or reuse the outbox worker flow
to publish the DynamoDB history and WebSocket notification after commit,
ensuring processing is idempotent and retain
publishRouteRecalculationRequestedAfterCommit only through that outbox
mechanism.
- Around line 43-45: RouteRecalculationService의 exists 후 save 흐름을 원자적으로 보호하세요.
RouteRecalculation 엔티티의 `@Table`(uniqueConstraints = ...)에 training_session_id,
trigger_edge_id, status 복합 유니크 제약을 추가하고, 저장 시 발생하는 유니크 제약 위반을 중복 이벤트로 처리해 관련 이력이
중복 생성되지 않도록 하세요.
In
`@src/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.java`:
- Around line 15-17: Enforce the one-RUNNING-session-per-building invariant in
the training session start/create path, covering both
TrainingSessionService.start() transitions and create(..., RUNNING) under
concurrency. Prefer a database constraint/index on the relevant training status
and scenario/building relationship, or add equivalent transactional locking;
also update findFirstByStatusAndScenario_Building_Id() with a deterministic
ordering so selection remains stable.
In
`@src/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java`:
- Around line 116-130: Update the test around
evacuationRouteService.findShortestRoute and capture its anySet() argument with
an argument captor or equivalent verification. Assert that the excluded-edge set
contains triggerEdge.getId(), while preserving the existing route recalculation
save and publish verifications.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: af0c0d0e-191f-45a5-a0f7-9a514ee97e05
📒 Files selected for processing (18)
src/main/java/com/saferoute/domain/congestion/controller/CongestionController.javasrc/main/java/com/saferoute/domain/congestion/dto/request/ReportCongestionRequest.javasrc/main/java/com/saferoute/domain/congestion/service/CongestionEventService.javasrc/main/java/com/saferoute/domain/evacuation/recalculation/entity/RecalculationStatus.javasrc/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.javasrc/main/java/com/saferoute/domain/evacuation/recalculation/repository/RouteRecalculationRepository.javasrc/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.javasrc/main/java/com/saferoute/domain/evacuation/service/EvacuationRouteService.javasrc/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.javasrc/main/java/com/saferoute/global/api/error/EvacuationErrorCode.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/CongestionEventData.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/RouteRecalculationEventData.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventType.javasrc/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.javasrc/test/java/com/saferoute/domain/congestion/controller/CongestionControllerTest.javasrc/test/java/com/saferoute/domain/congestion/service/CongestionEventServiceTest.javasrc/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.javasrc/test/java/com/saferoute/domain/evacuation/service/EvacuationRouteServiceTest.java
| RouteRecalculation recalculation = routeRecalculationRepository.save( | ||
| RouteRecalculation.createPending(session, triggerEdge, level, newPathNodeIds, route.totalWeight()) | ||
| ); | ||
|
|
||
| trainingEventRepository.save(TrainingEventItem.create( | ||
| session.getId().toString(), | ||
| UUID.randomUUID().toString(), | ||
| Instant.now().toEpochMilli(), | ||
| EventType.ROUTE_RECALCULATED, | ||
| TRIGGER_TYPE_CONGESTION, | ||
| List.of(triggerEdge.getId().toString()), | ||
| null, | ||
| newPathNodeIds.stream().map(UUID::toString).toList(), | ||
| null, | ||
| null, | ||
| null | ||
| )); | ||
|
|
||
| trainingEventPublisher.publishRouteRecalculationRequestedAfterCommit(recalculation); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
git ls-files | rg 'RouteRecalculationService|TrainingEvent|trainingEvent|recalculation|outbox|EventPublisher' || true
echo
echo "== target file outline =="
ast-grep outline src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java --view compact || true
echo
echo "== target lines =="
cat -n src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java | sed -n '1,140p'
echo
echo "== search relevant symbols =="
rg -n "trainingEventRepository|trainingEventPublisher|publishRouteRecalculationRequestedAfterCommit|RouteRecalculation|eventType|Dynamo|outbox|transactional|AfterCommit|TrainingEventItem|EventType" src/main/java -SRepository: DS-SafeRoute/SafeRoute-BE
Length of output: 25689
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== TrainingEventRepository =="
cat -n src/main/java/com/saferoute/domain/telemetry/dynamo/repository/TrainingEventRepository.java | sed -n '1,120p'
echo
echo "== TrainingEventPublisher =="
cat -n src/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.java | sed -n '1,220p'
echo
echo "== outbox/event publishing candidates =="
git ls-files | rg -i 'outbox|event|transaction|transactional|local|atomic|dynamodb' || true
echo
echo "== DynamoDB save transactional/propagation annotations =="
rg -n "`@Transactional`|TransactionTemplate|TransactionType|ReactiveTransactionManager|PlatformTransactionManager|propagation|afterCommit|commit|rollback" src/main/java -S
echo
echo "== RouteRecalculation entity/repository =="
cat -n src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.java | sed -n '1,130p'
cat -n src/main/java/com/saferoute/domain/evacuation/recalculation/repository/RouteRecalculationRepository.java | sed -n '1,80p'Repository: DS-SafeRoute/SafeRoute-BE
Length of output: 25005
JPA 트랜잭션과 DynamoDB 이력을 분리하세요.
routeRecalculationRepository.save(...)는 RouteRecalculationService.trigger()의 @Transactional에 참여하지만 trainingEventRepository.save(...)는 DynamoDB putItem을 곧바로 호출합니다. 만약 이후 JPA 커밋 실패로 트랜잭션이 롤백되면 DynamoDB에는 ROUTE_RECALCULATED 이력이 남는데 PENDING 행은 저장되지 않습니다.
재탐색 요청 이벤트 이력은 같은 JPA 트랜잭션에 outbox 레코드로 저장하세요. 커밋 후 워커가 DynamoDB 이력과 WebSocket 이벤트를 멱동적으로 발행하도록 변경하면 데이터 불일치를 막을 수 있습니다.
🤖 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/domain/evacuation/recalculation/service/RouteRecalculationService.java`
around lines 66 - 84, Update RouteRecalculationService.trigger() so the
ROUTE_RECALCULATED TrainingEvent is recorded as a JPA outbox entry within the
same transaction as routeRecalculationRepository.save(...), rather than calling
trainingEventRepository.save(...) directly. Add or reuse the outbox worker flow
to publish the DynamoDB history and WebSocket notification after commit,
ensuring processing is idempotent and retain
publishRouteRecalculationRequestedAfterCommit only through that outbox
mechanism.
Source: Path instructions
| // 혼잡 이벤트가 들어온 엣지가 속한 건물에 현재 진행 중인 세션이 있는지 조회한다. | ||
| // 건물당 동시 RUNNING 세션은 1개라고 가정한다 (TrainingSession에 세션-층 직접 연결이 없어 건물 단위로 조회). | ||
| Optional<TrainingSession> findFirstByStatusAndScenario_Building_Id(TrainingStatus status, UUID buildingId); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files 'src/main/java/**/*.java' | rg 'TrainingSession|LearningSession|Scenario|Congestion|Repository|Service' | sed -n '1,120p'
echo "== target repository =="
cat -n src/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.java
echo "== entities and scenario building relationships (targeted) =="
for f in $(git ls-files 'src/main/java/**/*.java' | rg 'TrainingSession|Scenario|Building'); do
echo "--- $f"
wc -l "$f"
ast-grep outline "$f" --view expanded 2>/dev/null | sed -n '1,160p'
done
echo "== usages of findFirstByStatusAndScenario_Building_Id =="
rg -n "findFirstByStatusAndScenario_Building_Id|findFirstByStatusAndScenario" src/main/java || true
echo "== methods around status transitions/session starts =="
rg -n "RUNNING|set.*Status|save\\(|delete\\(|TrainingSession|Scenario" src/main/java -g '*.java' | sed -n '1,240p'Repository: DS-SafeRoute/SafeRoute-BE
Length of output: 42765
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== TrainingSession entity =="
cat -n src/main/java/com/saferoute/domain/training/entity/TrainingSession.java
echo "== TrainingScenario entity relevant part =="
sed -n '1,135p' src/main/java/com/saferoute/domain/training/entity/TrainingScenario.java | cat -n
echo "== TrainingSessionService =="
cat -n src/main/java/com/saferoute/domain/training/service/TrainingSessionService.java
echo "== CongestionEventService =="
cat -n src/main/java/com/saferoute/domain/congestion/service/CongestionEventService.java
echo "== RouteRecalculationService =="
cat -n src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java
echo "== schema/migration files mentioning training scenario session =="
git ls-files | rg -i 'migration|schema|ddl|sql|migration|flyway|liquibase|changelog' | sed -n '1,200p' | xargs -r rg -n "training_session|training_scenario|scenario.*building|building_id|unique|constraint|key" || true
echo "== create session controller/dto =="
cat -n src/main/java/com/saferoute/domain/training/controller/TrainingSessionController.java
cat -n src/main/java/com/saferoute/domain/training/dto/CreateSessionRequest.java 2>/dev/null || trueRepository: DS-SafeRoute/SafeRoute-BE
Length of output: 28315
건물별 RUNNING 세션 단일성을 데이터 계층에서 enforce하세요.
TrainingSessionService.start()가 SCHEDULED → RUNNING로 전환할 때 동일 건물의 기존 RUNNING 세션을 reject하지 않아 create(..., RUNNING)과 동시 실행 시 여러 RUNNING 세션이 생성될 수 있습니다. findFirstByStatusAndScenario_Building_Id()에는 정렬 기준이 없으므로 혼잡 요약과 재탐색이 잘못된 세션에 연결될 수 있습니다. 세션 시작 경로에서 동시성 control을 추가하거나, training_sessions(training_status, training_scenario_id)의 unique index/constraint로 보장하세요.
🤖 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/domain/training/repository/TrainingSessionRepository.java`
around lines 15 - 17, Enforce the one-RUNNING-session-per-building invariant in
the training session start/create path, covering both
TrainingSessionService.start() transitions and create(..., RUNNING) under
concurrency. Prefer a database constraint/index on the relevant training status
and scenario/building relationship, or add equivalent transactional locking;
also update findFirstByStatusAndScenario_Building_Id() with a deterministic
ordering so selection remains stable.
Source: Path instructions
- ReportCongestionRequest에 headcount 음수 방지, avg<=peak, windowStart<windowEnd 검증 추가 - RouteRecalculation에 (session, edge, status) 유니크 제약 추가 및 동시 중복 저장 무시 처리 - 건물당 RUNNING 세션 조회 시 정렬 기준 추가로 결과 안정성 확보 - 재탐색 성공 테스트에서 제외 엣지 집합 검증 강화
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/test/java/com/saferoute/domain/congestion/controller/CongestionControllerTest.java (2)
53-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win성공 테스트에서 서비스 호출을 검증하세요.
reportCongestion_returnsOk()가CongestionController.doReportCongestion(...)가congestionEventService.reportCongestion(...)를 호출한다는 점을 검증하지 않아, 서비스 위임이 누락된 컨트롤러도 통과할 수 있습니다.verify(congestionEventService).reportCongestion(any(ReportCongestionRequest.class));🤖 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/domain/congestion/controller/CongestionControllerTest.java` around lines 53 - 60, Update reportCongestion_returnsOk() to verify that congestionEventService.reportCongestion(...) is called with a ReportCongestionRequest after the successful MockMvc request, while preserving the existing 200-status assertion.Source: Path instructions
29-36: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win관리를 위한 혼잡 이벤트 게시 요청에 권한 테스트를 추가하세요.
SecurityConfig에서/api/**POST 요청은 관리 역할이 필요합니다. 현재 테스트는 인증 필터와 보안 설정을 제외해 보안을 우회하므로, JWT 토큰 없이 401·403 응답을 검증하세요.🤖 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/domain/congestion/controller/CongestionControllerTest.java` around lines 29 - 36, Update CongestionControllerTest’s WebMvcTest configuration and add authorization tests for POST congestion-event requests: do not exclude SecurityConfig or disable its filters, then verify requests without a JWT return 401 and authenticated users lacking the administrator role return 403, while preserving the existing controller test coverage.Sources: Path instructions, MCP tools
🤖 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
`@src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java`:
- Around line 67-76: Update RouteRecalculationService.trigger() so the pending
recalculation save runs in a separate REQUIRES_NEW transaction with
READ_COMMITTED isolation, or delegate it to a separate transactional service.
Ensure the save is flushed within that transaction, swallow only duplicate
violations for uk_route_recalculation_session_edge_status, and propagate other
DataIntegrityViolationException cases without marking
CongestionEventService.reportCongestion()’s transaction rollback-only.
In
`@src/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java`:
- Around line 130-132: 경로 재계산 테스트에 routeRecalculationRepository.save(...)가
DataIntegrityViolationException을 발생시키는 동시 중복 저장 시나리오를 추가하세요. 해당 예외가 전파되거나 기대된
방식으로 처리되는지 검증하고, 그 경우 trainningEventRepository.save(...)와
publishRouteRecalculationRequestedAfterCommit(...)가 호출되지 않았음을 확인하세요. 기존 PENDING
상태 처리 테스트와 분리해 실제 저장 예외 분기를 별도로 검증하세요.
---
Outside diff comments:
In
`@src/test/java/com/saferoute/domain/congestion/controller/CongestionControllerTest.java`:
- Around line 53-60: Update reportCongestion_returnsOk() to verify that
congestionEventService.reportCongestion(...) is called with a
ReportCongestionRequest after the successful MockMvc request, while preserving
the existing 200-status assertion.
- Around line 29-36: Update CongestionControllerTest’s WebMvcTest configuration
and add authorization tests for POST congestion-event requests: do not exclude
SecurityConfig or disable its filters, then verify requests without a JWT return
401 and authenticated users lacking the administrator role return 403, while
preserving the existing controller test coverage.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c52ff9b1-6e8e-44b8-b8d7-d50b0b6fc41d
📒 Files selected for processing (8)
src/main/java/com/saferoute/domain/congestion/dto/request/ReportCongestionRequest.javasrc/main/java/com/saferoute/domain/congestion/service/CongestionEventService.javasrc/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.javasrc/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.javasrc/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.javasrc/test/java/com/saferoute/domain/congestion/controller/CongestionControllerTest.javasrc/test/java/com/saferoute/domain/congestion/service/CongestionEventServiceTest.javasrc/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/com/saferoute/domain/congestion/service/CongestionEventService.java
- src/test/java/com/saferoute/domain/congestion/service/CongestionEventServiceTest.java
- src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.java
| RouteRecalculation recalculation; | ||
| try { | ||
| recalculation = routeRecalculationRepository.save( | ||
| RouteRecalculation.createPending(session, triggerEdge, level, newPathNodeIds, route.totalWeight()) | ||
| ); | ||
| } catch (DataIntegrityViolationException exception) { | ||
| // 동시에 들어온 중복 혼잡 이벤트가 exists() 체크를 함께 통과한 경우, DB 유니크 제약으로 걸러진 것이므로 조용히 무시한다. | ||
| log.debug("동시 요청으로 인한 중복 재탐색 저장을 무시함: sessionId={}, edgeId={}", session.getId(), triggerEdge.getId()); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n '`@Transactional`|DataIntegrityViolationException|saveAndFlush|routeRecalculationRepository\.save|`@UniqueConstraint`' \
src/main/java --glob '*.java' || trueRepository: DS-SafeRoute/SafeRoute-BE
Length of output: 6342
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== RouteRecalculationService outline =="
ast-grep outline src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java --view expanded || true
echo
echo "== RouteRecalculationService relevant lines =="
cat -n src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java | sed -n '1,130p'
echo
echo "== CongestionEventService relevant lines =="
cat -n src/main/java/com/saferoute/domain/congestion/service/CongestionEventService.java | sed -n '1,140p'
echo
echo "== RouteRecalculation entity relevant lines =="
cat -n src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.java | sed -n '1,180p'
echo
echo "== Repository definitions/usages =="
rg -n "interface .*RouteRecalculation|class RouteRecalculation|trigger\(|reportCongestion|createPending|sessionId|edgeId|uniqueConstraints|UNIQUE KEY" src/main/java/com/saferoute --glob '*.java'Repository: DS-SafeRoute/SafeRoute-BE
Length of output: 35398
중복 저장 예외 처리를 현재 트랜잭션에서 삼키지 마세요.
CongestionEventService.reportCongestion()이 기본 @Transactional 상태에서 RouteRecalculationService.trigger()를 호출하므로, 저장에서 발생한 DataIntegrityViolationException 처리 후 현재 트랜잭션은 rollback-only가 됩니다. 이후 trainingEventRepository.save()가 실행되어 rollback-only 상태로 커밋을 시도하면 해당 트랜잭션 실행은 실패합니다.
현재 트랜잭션에서 save()만 호출하면 uk_route_recalculation_session_edge_status 제약 위반 SQL이 EntityManager.flush() 또는 커밋 시점에 실행되어 이 try 블록이 잡지 못합니다. 중복 insert만 허용하고 다른 통합 제약 위반은 다시 던지도록 Transactional(Propagation.REQUIRES_NEW, isolation = READ_COMMITTED) 처리나 별도 서비스 경계를 사용하세요.
🤖 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/domain/evacuation/recalculation/service/RouteRecalculationService.java`
around lines 67 - 76, Update RouteRecalculationService.trigger() so the pending
recalculation save runs in a separate REQUIRES_NEW transaction with
READ_COMMITTED isolation, or delegate it to a separate transactional service.
Ensure the save is flushed within that transaction, swallow only duplicate
violations for uk_route_recalculation_session_edge_status, and propagate other
DataIntegrityViolationException cases without marking
CongestionEventService.reportCongestion()’s transaction rollback-only.
Sources: Path instructions, MCP tools
| ArgumentCaptor<Set<UUID>> excludedEdgesCaptor = ArgumentCaptor.forClass(Set.class); | ||
| verify(evacuationRouteService).findShortestRoute(any(), any(), excludedEdgesCaptor.capture()); | ||
| assertThat(excludedEdgesCaptor.getValue()).containsExactly(triggerEdge.getId()); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the relevant test/service files without executing repository code.
fd -a 'RouteRecalculationServiceTest\.java|RouteRecalculationService\.java|routeRecalculationRepository\.java|trainingEventRepository\.java' . | while read -r f; do
echo "===== $f ====="
wc -l "$f"
done
echo "===== Search relevant symbols ====="
rg -n "routeRecalculationRepository|save\(|DataIntegrityViolationException|publishRouteRecalculationRequestedAfterCommit|trainingEventRepository" -S .Repository: DS-SafeRoute/SafeRoute-BE
Length of output: 12224
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== RouteRecalculationService.java ====="
cat -n src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java
echo "===== RouteRecalculationServiceTest.java ====="
cat -n src/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java
echo "===== Recalculation save method call sites in test ====="
rg -n "DataIntegrityViolationException|routeRecalculationRepository\.save|trainingEventRepository\.save|publishRouteRecalculationRequestedAfterCommit" src/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.javaRepository: DS-SafeRoute/SafeRoute-BE
Length of output: 13798
동시 중복 저장 예외 경로를 테스트하세요.
routeRecalculationRepository.save(...)가 DataIntegrityViolationException을 던질 때 trainningEventRepository.save(...)와 publishRouteRecalculationRequestedAfterCommit(...)가 호출되지 않아야 합니다. 같은 세션+엣지에 이미 존재하는 PENDING 상태는 성공 예외를 던지는 형태가 아니라 예외 분기가 아닌 경우도 있으므로, 실제 예외 발생 시 경로도 별도로 테스트해야 합니다.
🤖 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/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java`
around lines 130 - 132, 경로 재계산 테스트에 routeRecalculationRepository.save(...)가
DataIntegrityViolationException을 발생시키는 동시 중복 저장 시나리오를 추가하세요. 해당 예외가 전파되거나 기대된
방식으로 처리되는지 검증하고, 그 경우 trainningEventRepository.save(...)와
publishRouteRecalculationRequestedAfterCommit(...)가 호출되지 않았음을 확인하세요. 기존 PENDING
상태 처리 테스트와 분리해 실제 저장 예외 분기를 별도로 검증하세요.
Source: Path instructions
🔗 관련 이슈
closes #48
📋 작업 내용
혼잡 감지 시 자동 재탐색 트리거 로직 구현
POST /api/v1/congestion-events)CongestionSummaryItem)에 저장하고CONGESTION_UPDATEDWebSocket 이벤트 발행EvacuationRouteService에 제외 엣지 기반 우회 경로 탐색 오버로드 추가 (기존findShortestRoute(floorId, startNodeId)계약/테스트는 변경 없음)TrainingEventItem,EventType.ROUTE_RECALCULATED)에 저장ROUTE_RECALCULATION_REQUESTEDWebSocket 이벤트 발행 (트랜잭션 커밋 후)승인/거절 API 및
EVACUATION_ROUTE_UPDATED발행은 후속 이슈(#49)에서 별도로 진행 예정📄 API 변경사항
신규:
POST /api/v1/congestion-eventsRequest
{ "edgeId": "UUID (필수)", "cctvCode": "string", "avgHeadcount": "number", "peakHeadcount": "number", "congestionLevel": "LOW | MEDIUM | HIGH | CRITICAL (필수)", "windowStart": "long (필수, epoch)", "windowEnd": "long (필수, epoch)", "s3ImageKey": "string (nullable)" }Response:
200 OK, 바디 없음 (대상 건물에 RUNNING 세션이 없으면 아무 처리 없이 조용히 200 반환)Error
404 EVAC002 (MAP_EDGE_NOT_FOUND)— edgeId에 해당하는 엣지가 없을 때신규 에러 코드 (
EvacuationErrorCode)EVAC008 ROUTE_RECALCULATION_NOT_FOUNDEVAC009 INVALID_RECALCULATION_STATUS_TRANSITION(둘 다 이번 PR에서는 아직 사용처 없음 — #49에서 승인/거절 API가 소비 예정)
WebSocket 이벤트 (
/topic/training-sessions/{sessionId})CONGESTION_UPDATED— 혼잡 이벤트 수신 즉시 발행ROUTE_RECALCULATION_REQUESTED— 재탐색 결과가 PENDING으로 저장된 후(커밋 후) 발행(
EVACUATION_ROUTE_UPDATED는 타입만 미리 추가, 발행 로직은 #49)🧪 테스트 결과
./gradlew test전체 통과 (기존 테스트 포함 회귀 없음)CongestionEventServiceTest— 엣지 미존재/RUNNING 세션 없음/LOW·MEDIUM은 트리거 안 함/HIGH·CRITICAL은 트리거함RouteRecalculationServiceTest— 중복 PENDING 방지, 우회 경로 없을 때 스킵, 정상 트리거 시 저장+이벤트 발행CongestionControllerTest(@WebMvcTest) — 정상 요청 200, 필수값 누락 400, 서비스 예외 전파 404EvacuationRouteServiceTest— 제외 엣지 기반 우회 경로 계산 케이스 추가, 기존 케이스는 변경 없이 통과✅ 체크리스트
./gradlew build성공Summary by CodeRabbit
새 기능
개선
테스트