Skip to content

feat: 훈련 시작/종료 API 구현 - #40 - #41

Merged
hakSick merged 4 commits into
developfrom
feature/40-training-session-lifecycle
Aug 5, 2026
Merged

hakSick merged 4 commits into
developfrom
feature/40-training-session-lifecycle

Conversation

@kyeonG0210

@kyeonG0210 kyeonG0210 commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

##📌 관련 이슈
Closes #40

Summary

  • 훈련 세션 시작(POST /sessions/{sessionId}/start), 정상 종료(POST /sessions/{sessionId}/end), 강제 종료(POST /sessions/{sessionId}/force-end) API 구현
  • 10분 하드 타임아웃 스케줄러 추가 — RUNNING 세션 중 제한시간 초과 시 자동 FAILED 처리
  • 4개 상태 전이(시작/종료/강제종료/타임아웃) 지점 모두에서 TrainingEventPublisher를 통해 TRAINING_STATUS_UPDATED 웹소켓 이벤트 발행
  • 세션 생성 응답에 누락돼 있던 id 필드 추가
  • 세션 생성 시 관리자 권한(MANAGER) 검증 추가
  • TrainingSession에 낙관적 락(@Version)을 적용해 동시 상태 전이 시 충돌 방지
  • RUNNING 상태로 생성 요청 시 startedAt 누락 검증 추가

Test plan

  • TrainingSessionServiceTest — 상태별 전이 성공/실패, 세션 없음, 권한 검증, RUNNING 생성 시 startedAt 검증
  • TrainingSessionControllerTest — start/end/force-end MockMvc 테스트
  • TrainingTimeoutSchedulerTest — 스케줄러가 서비스에 위임하는지 확인
  • WebSocketIntegrationTest — 시작/종료/강제종료 각각 TRAINING_STATUS_UPDATED 이벤트 수신 확인
  • ./gradlew test 전체 통과

closes #40

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 훈련 세션의 시작, 종료, 강제 종료 기능 추가
    • 훈련 세션의 실시간 상태 업데이트 기능 추가
    • 10분 이상 실행된 세션의 자동 타임아웃 처리 기능 추가
  • 버그 수정

    • 세션 상태 변경 시 동시성 충돌 처리 개선
  • 테스트

    • 훈련 세션 API 및 스케줄링 기능 테스트 추가

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: ecd48bea-5c76-4469-87f2-bf886ad368c2

📥 Commits

Reviewing files that changed from the base of the PR and between 287f4e2 and aaf190d.

📒 Files selected for processing (2)
  • src/main/java/com/saferoute/domain/training/service/TrainingSessionService.java
  • src/test/java/com/saferoute/domain/training/service/TrainingSessionServiceTest.java
📝 Walkthrough

Walkthrough

훈련 세션의 시작·정상 종료·강제 종료 API를 추가했습니다. 세션 상태 이벤트를 발행하고, 10분 초과 세션을 스케줄러로 실패 처리합니다. 낙관적 잠금 오류와 관련 API 코드 및 테스트를 추가했습니다.

Changes

훈련 세션 생명주기

Layer / File(s) Summary
세션 상태 전이와 서비스 처리
src/main/java/com/saferoute/domain/training/..., src/main/java/com/saferoute/global/api/..., src/test/java/com/saferoute/domain/training/service/...
세션 응답에 ID를 추가했습니다. SCHEDULED → RUNNING, RUNNING → COMPLETED, RUNNING → STOPPED 전이를 구현했습니다. 낙관적 잠금과 훈련 오류·성공 코드를 추가했습니다. 상태 변경 후 이벤트를 발행하고 관련 서비스 테스트를 추가했습니다.
세션 제어 REST API와 검증
src/main/java/com/saferoute/domain/training/controller/TrainingSessionController.java, src/test/java/com/saferoute/domain/training/controller/TrainingSessionControllerTest.java
세션 시작, 정상 종료, 강제 종료 POST 엔드포인트를 추가했습니다. 표준 성공 응답과 상태 전이·세션 조회 오류 응답을 검증합니다.
타임아웃 스케줄 실행
src/main/java/com/saferoute/SafeRouteApplication.java, src/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.java, src/main/java/com/saferoute/domain/training/scheduler/TrainingTimeoutScheduler.java, src/main/java/com/saferoute/domain/training/service/TrainingSessionService.java, src/test/java/com/saferoute/domain/training/scheduler/TrainingTimeoutSchedulerTest.java, src/test/java/com/saferoute/domain/training/service/TrainingSessionServiceTest.java
스케줄링을 활성화했습니다. 30초마다 10분 초과 RUNNING 세션을 조회하고 FAILED로 변경합니다. 스케줄러 위임과 타임아웃 처리를 테스트합니다.
WebSocket 상태 이벤트 검증
src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java
세션별 토픽을 구독하고 시작·정상 종료·강제 종료에 따른 RUNNING, COMPLETED, STOPPED 이벤트를 검증합니다.

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

Possibly related issues

Possibly related PRs

Suggested labels: feat

Suggested reviewers: sunghyeon22

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 훈련 세션의 시작 및 종료 API 구현이라는 주요 변경 사항을 명확하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/40-training-session-lifecycle

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.

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

🧹 Nitpick comments (1)
src/test/java/com/saferoute/domain/training/controller/TrainingSessionControllerTest.java (1)

69-131: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

실패 응답 계약을 검증하십시오.

현재 실패 테스트는 HTTP 상태만 검사합니다. 실패 플래그 또는 API 오류 코드가 잘못되어도 테스트가 통과합니다. 각 실패 경로에서 전역 예외 응답의 실패 플래그와 TrainingErrorCode에 대응하는 오류 코드를 검증하십시오. end와 force-end의 TRAINING_SESSION_NOT_FOUND 경로도 추가하십시오.

🤖 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/training/controller/TrainingSessionControllerTest.java`
around lines 69 - 131, Update the failure tests around
startTrainingSession_invalidTransition_returnsConflict,
startTrainingSession_notFound_returns404,
endTrainingSession_invalidTransition_returnsConflict, and
forceEndTrainingSession_invalidTransition_returnsConflict to assert the global
error response’s failure flag and the error code corresponding to each
TrainingErrorCode, not only the HTTP status. Add TRAINING_SESSION_NOT_FOUND
failure tests for both endTrainingSession and forceEndTrainingSession with
matching status and error-body assertions.
🤖 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/training/service/TrainingSessionService.java`:
- Around line 85-125: Make the status validation and update atomic across start,
end, forceEnd, and timeout handling by applying locking to the TrainingSession
entity or its lookup method, using either `@Version` with conflict handling or a
pessimistic row lock. Ensure concurrent transitions cannot both succeed or
overwrite the final state, and apply the same protection to timeout-related
session reads.
- Around line 43-45: Update create in TrainingSessionService to validate that
the user loaded from request.getAdminId() has UserRole.MANAGER before passing it
to TrainingSession.create. Throw the appropriate existing API exception with its
error code when the role is not MANAGER, while preserving the current
ADMIN_NOT_FOUND behavior for missing users.

In `@src/main/resources/static/test.html`:
- Around line 39-40: Update the WebSocket URL default in the test page’s wsUrl
input to use the secure wss:// scheme instead of ws://, while retaining any
non-secure ws:// usage only through explicit approval or separate management.

In
`@src/test/java/com/saferoute/domain/training/service/TrainingSessionServiceTest.java`:
- Around line 116-127: Add missing-session tests for both
TrainingSessionService.end and forceEnd by stubbing findById(sessionId) with
Optional.empty(). Assert each call throws ApiException with
TRAINING_SESSION_NOT_FOUND and verify
trainingEventPublisher.publishTrainingStatusUpdatedAfterCommit is never invoked.

---

Nitpick comments:
In
`@src/test/java/com/saferoute/domain/training/controller/TrainingSessionControllerTest.java`:
- Around line 69-131: Update the failure tests around
startTrainingSession_invalidTransition_returnsConflict,
startTrainingSession_notFound_returns404,
endTrainingSession_invalidTransition_returnsConflict, and
forceEndTrainingSession_invalidTransition_returnsConflict to assert the global
error response’s failure flag and the error code corresponding to each
TrainingErrorCode, not only the HTTP status. Add TRAINING_SESSION_NOT_FOUND
failure tests for both endTrainingSession and forceEndTrainingSession with
matching status and error-body assertions.
🪄 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: 1ef655d4-e9a1-44ea-8334-a0cc4b54b1ed

📥 Commits

Reviewing files that changed from the base of the PR and between f206090 and 40ead7e.

📒 Files selected for processing (13)
  • src/main/java/com/saferoute/SafeRouteApplication.java
  • src/main/java/com/saferoute/domain/training/controller/TrainingSessionController.java
  • src/main/java/com/saferoute/domain/training/entity/TrainingSession.java
  • src/main/java/com/saferoute/domain/training/repository/TrainingSessionRepository.java
  • src/main/java/com/saferoute/domain/training/scheduler/TrainingTimeoutScheduler.java
  • src/main/java/com/saferoute/domain/training/service/TrainingSessionService.java
  • src/main/java/com/saferoute/global/api/error/TrainingErrorCode.java
  • src/main/java/com/saferoute/global/api/response/TrainingSuccessCode.java
  • src/main/resources/static/test.html
  • src/test/java/com/saferoute/domain/training/controller/TrainingSessionControllerTest.java
  • src/test/java/com/saferoute/domain/training/scheduler/TrainingTimeoutSchedulerTest.java
  • src/test/java/com/saferoute/domain/training/service/TrainingSessionServiceTest.java
  • src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java

Comment on lines +85 to +125
@Transactional
public TrainingSessionResponse start(UUID sessionId) {
TrainingSession session = findSession(sessionId);

if (session.getStatus() != TrainingStatus.SCHEDULED) {
throw new ApiException(TrainingErrorCode.INVALID_STATUS_TRANSITION);
}

session.start(Instant.now());
trainingEventPublisher.publishTrainingStatusUpdatedAfterCommit(session);

return TrainingSessionResponse.from(session);
}

@Transactional
public TrainingSessionResponse end(UUID sessionId) {
TrainingSession session = findSession(sessionId);

if (session.getStatus() != TrainingStatus.RUNNING) {
throw new ApiException(TrainingErrorCode.INVALID_STATUS_TRANSITION);
}

session.complete(Instant.now());
trainingEventPublisher.publishTrainingStatusUpdatedAfterCommit(session);

return TrainingSessionResponse.from(session);
}

@Transactional
public TrainingSessionResponse forceEnd(UUID sessionId) {
TrainingSession session = findSession(sessionId);

if (session.getStatus() != TrainingStatus.RUNNING) {
throw new ApiException(TrainingErrorCode.INVALID_STATUS_TRANSITION);
}

session.stop(Instant.now());
trainingEventPublisher.publishTrainingStatusUpdatedAfterCommit(session);

return TrainingSessionResponse.from(session);
}

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

상태 확인과 상태 갱신을 원자적으로 처리하십시오.

start, end, forceEnd, 타임아웃 처리는 상태를 읽고 검증한 뒤 갱신합니다. 행 잠금과 @Version이 없으므로 두 트랜잭션이 같은 RUNNING 상태를 동시에 읽을 수 있습니다. 예를 들어 정상 종료와 강제 종료가 모두 성공한 뒤 마지막 커밋이 최종 상태를 덮어쓸 수 있습니다. 모든 상태 전이와 타임아웃 조회에 낙관적 잠금과 충돌 처리 또는 비관적 행 잠금을 적용하십시오.

Also applies to: 127-138

🤖 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/service/TrainingSessionService.java`
around lines 85 - 125, Make the status validation and update atomic across
start, end, forceEnd, and timeout handling by applying locking to the
TrainingSession entity or its lookup method, using either `@Version` with conflict
handling or a pessimistic row lock. Ensure concurrent transitions cannot both
succeed or overwrite the final state, and apply the same protection to
timeout-related session reads.

Comment thread src/main/resources/static/test.html Outdated
Comment on lines +39 to +40
<label>WebSocket URL</label>
<input id="wsUrl" value="ws://localhost:8080/ws" />

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate test.html =="
fd -a 'test\.html$' . || true

echo "== file outline/size =="
if [ -f src/main/resources/static/test.html ]; then
  wc -l src/main/resources/static/test.html
  ast-grep outline src/main/resources/static/test.html || true
  echo "== relevant slices =="
  sed -n '1,120p' src/main/resources/static/test.html | cat -n
fi

echo "== search wsUrl/STOMP/websocket usages in repository =="
rg -n "wsUrl|STOMP|new SockJS|addEventListener\\(|ws://|wss://" . --glob '!build/**' --glob '!target/**' --glob '!node_modules/**' || true

echo "== inspect java websocket/stomp related files =="
fd -i 'websocket|stomp|config|security' . || true

Repository: DS-SafeRoute/SafeRoute-BE

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== static path/config/security files =="
fd -i 'SecurityConfig|WebSecurity|Security|WebSocketConfig|WebSocketMessage|WebSocket|test.html|test.*html' src/main src/test | sort

echo "== SecurityConfig =="
fd -i 'SecurityConfig' src/main/java | while read -r f; do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,220p' "$f" | cat -n
done

echo "== Static resource handling =="
rg -n "addResourceHandlers|WebMvcConfigurer|ResourceHandlerRegistry|static|classPathResource|spring.web.resources|AuthorizationRequestMapper|authorizeHttpRequests|HttpSecurity" src/main/java --glob '*.java' || true

echo "== WebSocket authorization/acceptance =="
sed -n '1,180p' src/main/java/com/saferoute/infrastructure/websocket/config/WebSocketConfig.java | cat -n
sed -n '1,120p' src/main/java/com/saferoute/infrastructure/websocket/security/StompAuthChannelInterceptor.java | cat -n
sed -n '1,340p' src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java | cat -n

echo "== test.html static route access hints =="
rg -n "test.html|static/test|/test|/session/\\{/webjars/\" " src/main/resources src/main/java --glob '*.java' --glob '*.yml' --glob '*.yaml' --glob '*.properties' || true

echo "== small AST parse of connect function and brokerURL handling =="
python3 - <<'PY'
from pathlib import Path
p = Path('src/main/resources/static/test.html')
s = p.read_text(errors='ignore')
start = s.index('function connect()')
end = s.index('function disconnect()')
print(s[start:end])
print("has ws prefix validation:", 'wsUrl.startsWith("wss://")' in s or "wsUrl.startsWith('wss://')" in s)
print("has ws prefix validation regex:", 'wsUrl.match' in s)
PY

Repository: DS-SafeRoute/SafeRoute-BE

Length of output: 41511


Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information

Reachability: External

이 테스트 페이지의 ws:// 연결을 운영 환경에 내놓지 마세요.

ws://에서는 Authorization: Bearer ... 헤더가 평신호 전송 구간으로 내려갑니다. 테스트용 페이지라 해도 wss://를 기본으로 설정하고, ws://는 명시적으로 승인하는 등 별도 관리가 필요합니다.

🤖 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/resources/static/test.html` around lines 39 - 40, Update the
WebSocket URL default in the test page’s wsUrl input to use the secure wss://
scheme instead of ws://, while retaining any non-secure ws:// usage only through
explicit approval or separate management.

TrainingSessionResponse에 세션 UUID(id)가 빠져 있어, 세션 생성 후
start/end/force-end 호출에 필요한 sessionId를 응답에서 알 수 없던 문제 수정.
웹소켓 수동 테스트용 test.html은 더 이상 필요 없어 삭제.
- create()에서 관리자 역할이 MANAGER가 아니면 FORBIDDEN 예외를 던지도록 검증 추가
- TrainingSession에 @Version을 추가해 start/end/force-end/timeout 간 동시 상태
  전이 시 하나만 성공하고 나머지는 낙관적 락 충돌로 실패하도록 변경
- ObjectOptimisticLockingFailureException을 409 CONFLICT로 매핑하는
  전역 예외 핸들러 추가
- end/forceEnd의 세션 없음 케이스 테스트 추가
- WebSocketIntegrationTest에서 서비스가 갱신한 stale 엔티티를 delete()하며
  발생하던 낙관적 락 충돌을 deleteById()로 수정

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

🧹 Nitpick comments (1)
src/main/java/com/saferoute/domain/training/service/TrainingSessionService.java (1)

132-144: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

타임아웃 처리를 제한된 배치로 실행하십시오.

Line 136-137은 모든 타임아웃 세션을 List로 읽습니다. Line 139-143은 모든 세션을 하나의 트랜잭션에서 갱신하고 세션마다 커밋 후 이벤트를 등록합니다.

스케줄러 중단 후 backlog가 증가하면 메모리 사용량, flush 시간, 잠금 유지 시간, 이벤트 콜백 수가 함께 증가할 수 있습니다. repository 조회에 Pageable 또는 limit을 적용하십시오. 제한된 크기의 chunk를 별도 트랜잭션으로 처리하십시오.

As per path instructions, 트랜잭션 경계와 데이터 접근 부하를 중점적으로 확인해야 합니다.

🤖 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/service/TrainingSessionService.java`
around lines 132 - 144, failTimedOutSessions가 모든 타임아웃 세션을 한 번에 조회·처리하지 않도록
trainingSessionRepository 조회에 Pageable 또는 limit을 적용하십시오. 제한된 chunk 단위로 별도 트랜잭션에서
세션을 갱신하고, 각 세션의 상태 변경·publishTrainingStatusUpdatedAfterCommit·로그 처리는 유지하십시오.
스케줄러가 계속 backlog를 처리할 수 있도록 chunk별 트랜잭션 경계가 실제로 분리되게 구성하십시오.

Source: Path instructions

🤖 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/training/service/TrainingSessionService.java`:
- Around line 45-52: Ensure the RUNNING status always has a non-null startedAt
when creating a session. Add validation in TrainingSessionService.create for
CreateSessionRequest, or enforce the invariant in TrainingSession.create by
rejecting RUNNING with a null startedAt, while preserving start()’s existing
transition behavior.

---

Nitpick comments:
In
`@src/main/java/com/saferoute/domain/training/service/TrainingSessionService.java`:
- Around line 132-144: failTimedOutSessions가 모든 타임아웃 세션을 한 번에 조회·처리하지 않도록
trainingSessionRepository 조회에 Pageable 또는 limit을 적용하십시오. 제한된 chunk 단위로 별도 트랜잭션에서
세션을 갱신하고, 각 세션의 상태 변경·publishTrainingStatusUpdatedAfterCommit·로그 처리는 유지하십시오.
스케줄러가 계속 backlog를 처리할 수 있도록 chunk별 트랜잭션 경계가 실제로 분리되게 구성하십시오.
🪄 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: ca529d67-85df-4a58-8e5e-02a93fac4ed9

📥 Commits

Reviewing files that changed from the base of the PR and between 40ead7e and 287f4e2.

📒 Files selected for processing (6)
  • src/main/java/com/saferoute/domain/training/dto/TrainingSessionResponse.java
  • src/main/java/com/saferoute/domain/training/entity/TrainingSession.java
  • src/main/java/com/saferoute/domain/training/service/TrainingSessionService.java
  • src/main/java/com/saferoute/global/api/exception/GlobalExceptionHandler.java
  • src/test/java/com/saferoute/domain/training/service/TrainingSessionServiceTest.java
  • src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/com/saferoute/domain/training/entity/TrainingSession.java
  • src/test/java/com/saferoute/infrastructure/websocket/service/WebSocketIntegrationTest.java
  • src/test/java/com/saferoute/domain/training/service/TrainingSessionServiceTest.java

컨트롤러가 @Valid 없이 CreateSessionRequest를 받아 startedAt의 @NotNull이
실제로는 적용되지 않던 문제. RUNNING 상태로 생성 요청 시 startedAt이
null이면 INVALID_INPUT을 던지도록 서비스 레이어에 검증 추가.
@hakSick
hakSick merged commit 304a614 into develop Aug 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[✨ Feature] 훈련 시작/종료(정상·강제·타임아웃) API 구현 및 WebSocket 연동

2 participants