Skip to content

feat: 혼잡 이벤트 수신 및 자동 재탐색 트리거 구현 - #50

Merged
songmin0111 merged 2 commits into
developfrom
feature/48-congestion-recalculation-trigger
Aug 9, 2026
Merged

songmin0111 merged 2 commits into
developfrom
feature/48-congestion-recalculation-trigger

Conversation

@kyeonG0210

@kyeonG0210 kyeonG0210 commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

🔗 관련 이슈

closes #48

📋 작업 내용

혼잡 감지 시 자동 재탐색 트리거 로직 구현

  • CCTV 혼잡 이벤트 수신 API 구현 (POST /api/v1/congestion-events)
  • 혼잡도 요약을 DynamoDB(CongestionSummaryItem)에 저장하고 CONGESTION_UPDATED WebSocket 이벤트 발행
  • HIGH/CRITICAL 수준이면 우회 경로를 자동 재탐색해 "승인 대기(PENDING)" 상태로 저장
    • EvacuationRouteService에 제외 엣지 기반 우회 경로 탐색 오버로드 추가 (기존 findShortestRoute(floorId, startNodeId) 계약/테스트는 변경 없음)
    • 같은 세션+엣지에 이미 PENDING이 있으면 중복 트리거 방지
    • 우회 경로 자체가 없으면 로그만 남기고 승인 대기 항목을 만들지 않음
    • 재탐색 이력을 DynamoDB(TrainingEventItem, EventType.ROUTE_RECALCULATED)에 저장
    • ROUTE_RECALCULATION_REQUESTED WebSocket 이벤트 발행 (트랜잭션 커밋 후)

승인/거절 API 및 EVACUATION_ROUTE_UPDATED 발행은 후속 이슈(#49)에서 별도로 진행 예정

📄 API 변경사항

신규: POST /api/v1/congestion-events

Request

{
  "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_FOUND
  • EVAC009 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, 서비스 예외 전파 404
    • EvacuationRouteServiceTest — 제외 엣지 기반 우회 경로 계산 케이스 추가, 기존 케이스는 변경 없이 통과
  • 실제 EC2(Instance Profile 권한) + RDS + DynamoDB 환경에서 수동 end-to-end 테스트 완료 — 우회 경로 있는 케이스(PENDING 생성 확인)와 없는 케이스(스킵 확인) 둘 다 검증함

✅ 체크리스트

  • 셀프 코드리뷰 완료
  • 컨벤션 준수
  • 테스트 통과
  • ./gradlew build 성공

Summary by CodeRabbit

  • 새 기능

    • 혼잡 이벤트를 신고하고 혼잡도·인원·발생 시간 정보를 전송할 수 있습니다.
    • 혼잡 현황이 실시간으로 업데이트되며, 높은 혼잡도 감지 시 우회 경로 재계산 요청이 생성됩니다.
    • 재계산된 우회 경로와 처리 상태를 실시간으로 확인할 수 있습니다.
    • 특정 구간을 제외한 대체 대피 경로를 계산할 수 있습니다.
  • 개선

    • 잘못된 요청이나 존재하지 않는 지도 구간에 대해 명확한 오류 응답을 제공합니다.
    • 혼잡 이벤트와 우회 경로 알림의 전달 시점과 안정성이 개선되었습니다.
  • 테스트

    • 혼잡 신고, 경로 재계산, 대체 경로 계산 시나리오를 검증했습니다.

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 이벤트 발행 추가
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

혼잡 이벤트 수신 API와 검증 DTO를 추가했다. 실행 중인 세션에 혼잡 요약과 WebSocket 이벤트를 저장한다. HIGH 또는 CRITICAL이면 제외 엣지를 반영한 우회 경로를 계산하고 PENDING 재탐색을 생성한다.

Changes

혼잡 이벤트 수신 및 처리

Layer / File(s) Summary
혼잡 신고 계약과 API 진입점
src/main/java/com/saferoute/domain/congestion/dto/request/ReportCongestionRequest.java, src/main/java/com/saferoute/domain/congestion/controller/CongestionController.java
요청 필드를 검증하고 POST /api/v1/congestion-events에서 서비스로 전달한다.
혼잡 이벤트 처리와 WebSocket 발행
src/main/java/com/saferoute/domain/congestion/service/CongestionEventService.java, src/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.java, src/main/java/com/saferoute/infrastructure/websocket/dto/CongestionEventData.java, src/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventType.java, src/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.java, src/test/java/com/saferoute/domain/congestion/controller/CongestionControllerTest.java, src/test/java/com/saferoute/domain/congestion/service/CongestionEventServiceTest.java
간선과 RUNNING 세션을 조회한다. 세션이 있으면 혼잡 요약을 저장하고 CONGESTION_UPDATED를 발행한다. HIGH 또는 CRITICAL이면 재탐색을 호출한다.

우회 경로 계산 및 재탐색 상태

Layer / File(s) Summary
재탐색 엔티티와 경로 계산 계약
src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RecalculationStatus.java, src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.java, src/main/java/com/saferoute/domain/evacuation/recalculation/repository/RouteRecalculationRepository.java, src/main/java/com/saferoute/domain/evacuation/service/EvacuationRouteService.java, src/main/java/com/saferoute/global/api/error/EvacuationErrorCode.java, src/test/java/com/saferoute/domain/evacuation/service/EvacuationRouteServiceTest.java
재탐색 상태와 엔티티를 추가했다. 지정 엣지를 제외한 경로 계산과 PENDING 중복 확인을 지원한다.

재탐색 오케스트레이션 및 알림

Layer / File(s) Summary
자동 재탐색 처리
src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java, src/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java
동일 세션·엣지의 PENDING 항목을 확인한다. 우회 경로가 있으면 재탐색과 교육 이벤트를 저장한다. 경로가 없으면 저장하지 않는다.
재탐색 WebSocket 이벤트
src/main/java/com/saferoute/infrastructure/websocket/dto/RouteRecalculationEventData.java, src/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.java
ROUTE_RECALCULATION_REQUESTED 이벤트와 트랜잭션 커밋 후 발행을 추가한다.

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(...)
Loading

Possibly related PRs

Suggested reviewers: haksick

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 대부분의 #48 요구사항을 구현했지만, uq_route_recalculation_pending 부분 유니크 인덱스 마이그레이션이 변경 목록에서 확인되지 않습니다. 동일 세션·엣지의 PENDING 중복을 데이터베이스에서 차단하는 uq_route_recalculation_pending 부분 유니크 인덱스 마이그레이션을 추가하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% 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 제목은 혼잡 이벤트 수신과 자동 재탐색 트리거라는 PR의 핵심 변경을 정확히 설명합니다.
Out of Scope Changes check ✅ Passed 변경 사항은 #48의 혼잡 이벤트 처리, 자동 재탐색, 저장, WebSocket 이벤트 및 테스트 범위에 포함됩니다.
✨ 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 feature/48-congestion-recalculation-trigger

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.

@kyeonG0210 kyeonG0210 self-assigned this Aug 7, 2026
@kyeonG0210 kyeonG0210 added the feat 기능 개발 label Aug 7, 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

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 48ef85d and fef1396.

📒 Files selected for processing (18)
  • src/main/java/com/saferoute/domain/congestion/controller/CongestionController.java
  • src/main/java/com/saferoute/domain/congestion/dto/request/ReportCongestionRequest.java
  • src/main/java/com/saferoute/domain/congestion/service/CongestionEventService.java
  • src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RecalculationStatus.java
  • src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.java
  • src/main/java/com/saferoute/domain/evacuation/recalculation/repository/RouteRecalculationRepository.java
  • src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java
  • src/main/java/com/saferoute/domain/evacuation/service/EvacuationRouteService.java
  • src/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.java
  • src/main/java/com/saferoute/global/api/error/EvacuationErrorCode.java
  • src/main/java/com/saferoute/infrastructure/websocket/dto/CongestionEventData.java
  • src/main/java/com/saferoute/infrastructure/websocket/dto/RouteRecalculationEventData.java
  • src/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventType.java
  • src/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.java
  • src/test/java/com/saferoute/domain/congestion/controller/CongestionControllerTest.java
  • src/test/java/com/saferoute/domain/congestion/service/CongestionEventServiceTest.java
  • src/test/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationServiceTest.java
  • src/test/java/com/saferoute/domain/evacuation/service/EvacuationRouteServiceTest.java

Comment on lines +66 to +84
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);

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.

🗄️ 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 -S

Repository: 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

Comment on lines +15 to +17
// 혼잡 이벤트가 들어온 엣지가 속한 건물에 현재 진행 중인 세션이 있는지 조회한다.
// 건물당 동시 RUNNING 세션은 1개라고 가정한다 (TrainingSession에 세션-층 직접 연결이 없어 건물 단위로 조회).
Optional<TrainingSession> findFirstByStatusAndScenario_Building_Id(TrainingStatus status, UUID buildingId);

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.

🗄️ 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 || true

Repository: 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 세션 조회 시 정렬 기준 추가로 결과 안정성 확보
- 재탐색 성공 테스트에서 제외 엣지 집합 검증 강화

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between fef1396 and 3c0ac4f.

📒 Files selected for processing (8)
  • src/main/java/com/saferoute/domain/congestion/dto/request/ReportCongestionRequest.java
  • src/main/java/com/saferoute/domain/congestion/service/CongestionEventService.java
  • src/main/java/com/saferoute/domain/evacuation/recalculation/entity/RouteRecalculation.java
  • src/main/java/com/saferoute/domain/evacuation/recalculation/service/RouteRecalculationService.java
  • src/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.java
  • src/test/java/com/saferoute/domain/congestion/controller/CongestionControllerTest.java
  • src/test/java/com/saferoute/domain/congestion/service/CongestionEventServiceTest.java
  • src/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

Comment on lines +67 to +76
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;
}

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.

🗄️ 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' || true

Repository: 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

Comment on lines +130 to +132
ArgumentCaptor<Set<UUID>> excludedEdgesCaptor = ArgumentCaptor.forClass(Set.class);
verify(evacuationRouteService).findShortestRoute(any(), any(), excludedEdgesCaptor.capture());
assertThat(excludedEdgesCaptor.getValue()).containsExactly(triggerEdge.getId());

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 | 🟡 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.java

Repository: 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

@songmin0111
songmin0111 merged commit 093f9b3 into develop Aug 9, 2026
2 checks passed
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] 혼잡 이벤트 수신 및 자동 재탐색 트리거

2 participants