[CHORE] Swagger @Tag로 API 그룹핑 - #47
Conversation
관리자가 유도등 방향(LEFT/RIGHT/OFF)을 라즈베리파이에 명령하고, 결과를
층 도면 화면에 웹소켓으로 실시간 반영할 수 있도록 구현.
- PATCH /lights/{id}/pi-endpoint: 유도등에 라즈베리파이 주소 설정
- PATCH /lights/{id}/direction: 방향 명령 (검증 -> Pi 호출 -> 상태 저장 -> 이벤트 발행)
- IoTLightPiClient: WebClient 기반 Pi 통신 (3초 타임아웃, 실패 시 DEVICE_UNREACHABLE)
- IoTLightDirectionStore: 실시간 방향 상태를 서버 메모리에서 관리 (DB 미저장)
- TrainingEventPublisher.publishIoTLightStatusUpdated(AfterCommit): 기존 발행 클래스에
유도등 이벤트 메서드 추가, /topic/floors/{floorId}/lights로 발행
- StompAuthChannelInterceptor: 층 유도등 topic 구독 인가 로직 추가
- 서비스/컨트롤러/발행자/웹소켓 통합 테스트 추가
…락 수정 PR #43에서 추가된 AiAnalysisClient가 saferoute.ai-service.base-url 프로퍼티를 필수로 요구하는데, src/test/resources/application.yml에는 이 값이 없어 모든 @SpringBootTest 전체 컨텍스트 테스트가 PlaceholderResolutionException으로 실패하고 있었다. src/test/resources/application.yml이 클래스패스에서 src/main/resources/application.yml과 파일명이 같아 완전히 가려버려서 메인 쪽 기본값도 적용되지 않는 것이 원인이었다. S3Config 더미값과 동일한 방식으로 더미 base-url을 추가해 해결. WebSocketIntegrationTest 7개 테스트가 다시 통과함을 확인.
publishTrainingStatusUpdated()에 "호출하는 지점이 없다"는 주석이 남아있었는데, 지금은 훈련 시작/종료/강제종료/타임아웃 4곳에서 publishTrainingStatusUpdatedAfterCommit을 통해 이미 호출되고 있어 사실과 다른 내용이었음. 실제 동작 기준으로 갱신.
saferoute: 하위에 잘못 위치해 있어 @value("${aws.dynamodb.table-name}")를 참조하는 DynamoDB 리포지토리들의 빈 생성이 실패하던 문제 수정
Swagger UI에서 도메인별로 API를 구분해서 볼 수 있도록 각 컨트롤러에 @tag(name, description) 추가
📝 WalkthroughWalkthroughIoT 유도등의 Raspberry Pi 방향 제어 API, 엔드포인트 설정, 메모리 상태 저장, 층별 WebSocket 이벤트를 추가했습니다. 여러 컨트롤러에 Swagger 태그를 등록하고 관련 검증 및 통합 테스트를 확장했습니다. ChangesIoT 유도등 제어 및 이벤트
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant IoTLightController
participant IoTLightService
participant IoTLightPiClient
participant IoTLightDirectionStore
participant TrainingEventPublisher
IoTLightController->>IoTLightService: 방향 변경 요청
IoTLightService->>IoTLightPiClient: Raspberry Pi 방향 명령 전송
IoTLightService->>IoTLightDirectionStore: 방향 상태 저장
IoTLightService->>TrainingEventPublisher: 커밋 후 상태 이벤트 발행
IoTLightService-->>IoTLightController: 방향 변경 응답
🚥 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: 2
🤖 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/device/service/IoTLightService.java`:
- Around line 152-154: Serialize commands per lightId in the flow containing
iotLightPiClient.sendDirection, iotLightDirectionStore.update, and
trainingEventPublisher.publishIoTLightStatusUpdatedAfterCommit so each light’s
Pi call, state update, and event registration execute in request order. Use a
per-light command queue or equivalent version/order validation, while allowing
commands for different lights to proceed independently and preventing stale
responses from overwriting state or publishing outdated events.
In
`@src/test/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisherTest.java`:
- Around line 146-150: Update the assertions for the IoT light status event in
TrainingEventPublisherTest to also verify that message.data().updatedAt() is not
null, alongside the existing envelope occurredAt check.
🪄 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: 3e86f3f2-4231-4b82-a7e7-a9a96bdddef5
📒 Files selected for processing (36)
build.gradlesrc/main/java/com/saferoute/domain/analysis/controller/AnalysisController.javasrc/main/java/com/saferoute/domain/building/controller/BuildingController.javasrc/main/java/com/saferoute/domain/dashboard/DashboardController.javasrc/main/java/com/saferoute/domain/device/client/IoTLightPiClient.javasrc/main/java/com/saferoute/domain/device/client/PiLightDirectionRequest.javasrc/main/java/com/saferoute/domain/device/controller/IoTLightController.javasrc/main/java/com/saferoute/domain/device/dto/request/ChangeLightDirectionRequest.javasrc/main/java/com/saferoute/domain/device/dto/request/UpdatePiEndpointRequest.javasrc/main/java/com/saferoute/domain/device/dto/response/IoTLightResponse.javasrc/main/java/com/saferoute/domain/device/dto/response/LightDirectionResponse.javasrc/main/java/com/saferoute/domain/device/entity/IoTLight.javasrc/main/java/com/saferoute/domain/device/service/IoTLightDirectionStore.javasrc/main/java/com/saferoute/domain/device/service/IoTLightService.javasrc/main/java/com/saferoute/domain/evacuation/controller/EvacuationRouteController.javasrc/main/java/com/saferoute/domain/evacuation/controller/MapGraphController.javasrc/main/java/com/saferoute/domain/evacuation/controller/MapGraphEditController.javasrc/main/java/com/saferoute/domain/floor/controller/FloorController.javasrc/main/java/com/saferoute/domain/report/controller/TrainingReportController.javasrc/main/java/com/saferoute/domain/training/controller/TrainingScenarioController.javasrc/main/java/com/saferoute/domain/training/controller/TrainingSessionController.javasrc/main/java/com/saferoute/domain/user/controller/UserController.javasrc/main/java/com/saferoute/global/api/error/IoTLightErrorCode.javasrc/main/java/com/saferoute/global/api/response/IoTLightSuccessCode.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/IoTLightEventMessage.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/IoTLightStatusEventData.javasrc/main/java/com/saferoute/infrastructure/websocket/dto/TrainingEventType.javasrc/main/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptor.javasrc/main/java/com/saferoute/infrastructure/websocket/service/TrainingEventPublisher.javasrc/main/resources/application.ymlsrc/test/java/com/saferoute/domain/device/controller/IoTLightControllerTest.javasrc/test/java/com/saferoute/domain/device/service/IoTLightServiceTest.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
| iotLightPiClient.sendDirection(light.getPiEndpoint(), light.getCode(), direction); | ||
| iotLightDirectionStore.update(light.getId(), direction); | ||
| trainingEventPublisher.publishIoTLightStatusUpdatedAfterCommit(light, direction); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
유도등별 명령 순서를 직렬화하세요.
동일 lightId에 대한 두 요청은 동시에 Pi 호출을 수행할 수 있습니다. 이전 요청의 Pi 응답이 늦게 도착하면 최신 요청이 저장한 방향을 이전 방향으로 덮어쓰고, 오래된 상태 이벤트를 발행할 수 있습니다. Pi의 실제 방향과 메모리 상태 및 WebSocket 이벤트가 불일치합니다.
Pi 응답 수신, 상태 저장, 이벤트 등록을 유도등별 단일 명령 큐 또는 순서 보장 가능한 버전 처리로 직렬화하세요.
🤖 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/device/service/IoTLightService.java`
around lines 152 - 154, Serialize commands per lightId in the flow containing
iotLightPiClient.sendDirection, iotLightDirectionStore.update, and
trainingEventPublisher.publishIoTLightStatusUpdatedAfterCommit so each light’s
Pi call, state update, and event registration execute in request order. Use a
per-light command queue or equivalent version/order validation, while allowing
commands for different lights to proceed independently and preventing stale
responses from overwriting state or publishing outdated events.
| assertThat(message.eventType()).isEqualTo(TrainingEventType.IOT_LIGHT_STATUS_UPDATED); | ||
| assertThat(message.floorId()).isEqualTo(floorId); | ||
| assertThat(message.occurredAt()).isNotNull(); | ||
| assertThat(message.data().lightId()).isEqualTo(light.getId()); | ||
| assertThat(message.data().direction()).isEqualTo(IoTLightDirection.LEFT); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
data.updatedAt을 검증하세요.
현재 테스트는 envelope의 occurredAt만 검증합니다. IoTLightStatusEventData.updatedAt이 null이어도 테스트가 통과합니다.
수정 예시
assertThat(message.occurredAt()).isNotNull();
assertThat(message.data().lightId()).isEqualTo(light.getId());
assertThat(message.data().direction()).isEqualTo(IoTLightDirection.LEFT);
+ assertThat(message.data().updatedAt()).isNotNull();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| assertThat(message.eventType()).isEqualTo(TrainingEventType.IOT_LIGHT_STATUS_UPDATED); | |
| assertThat(message.floorId()).isEqualTo(floorId); | |
| assertThat(message.occurredAt()).isNotNull(); | |
| assertThat(message.data().lightId()).isEqualTo(light.getId()); | |
| assertThat(message.data().direction()).isEqualTo(IoTLightDirection.LEFT); | |
| assertThat(message.eventType()).isEqualTo(TrainingEventType.IOT_LIGHT_STATUS_UPDATED); | |
| assertThat(message.floorId()).isEqualTo(floorId); | |
| assertThat(message.occurredAt()).isNotNull(); | |
| assertThat(message.data().lightId()).isEqualTo(light.getId()); | |
| assertThat(message.data().direction()).isEqualTo(IoTLightDirection.LEFT); | |
| assertThat(message.data().updatedAt()).isNotNull(); |
🤖 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/TrainingEventPublisherTest.java`
around lines 146 - 150, Update the assertions for the IoT light status event in
TrainingEventPublisherTest to also verify that message.data().updatedAt() is not
null, alongside the existing envelope occurredAt check.
🔗 관련 이슈
closes #46
📋 작업 내용
🧪 테스트 결과
Summary by CodeRabbit
새로운 기능
문서
오류 처리