Skip to content

Commit a096dca

Browse files
committed
fix: PR review 반영
1 parent 97b4742 commit a096dca

4 files changed

Lines changed: 49 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
2424

2525
### Package Structure
2626

27-
```
27+
```text
2828
com.semosan.api
2929
├── common/ # 공통 인프라 (응답, 예외, JWT, 설정, FCM, 알림 공통)
3030
└── domain/ # 도메인별 비즈니스 로직
@@ -108,7 +108,7 @@ prod에서 필요한 주요 변수: `DB_URL`, `DB_USERNAME`, `DB_PASSWORD`, `RED
108108
## Branch & Commit Convention
109109

110110
브랜치명:
111-
```
111+
```text
112112
feat/#이슈번호-기능명
113113
fix/#이슈번호-버그명
114114
```

src/main/java/com/semosan/api/domain/tracking/controller/docs/TrackingControllerDocs.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ ResponseEntity<ApiResponse<NearbyMountainResponse>> getNearbyMountain(
5858
responseCode = "404",
5959
description = "코스를 찾을 수 없음",
6060
content = @Content(schema = @Schema(implementation = ApiResponse.class))
61+
),
62+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
63+
responseCode = "422",
64+
description = "코스 경로 좌표가 등록되지 않음",
65+
content = @Content(schema = @Schema(implementation = ApiResponse.class))
6166
)
6267
})
6368
ResponseEntity<ApiResponse<LiveActivityCourseResponse>> getLiveActivityCourse(

src/main/java/com/semosan/api/domain/tracking/service/TrackingSessionService.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.semosan.api.domain.user.service.UserReader;
2121
import lombok.RequiredArgsConstructor;
2222
import lombok.extern.slf4j.Slf4j;
23+
import org.hibernate.exception.ConstraintViolationException;
2324
import org.springframework.context.ApplicationEventPublisher;
2425
import org.springframework.dao.DataIntegrityViolationException;
2526
import org.springframework.stereotype.Service;
@@ -33,6 +34,8 @@
3334
@Transactional(readOnly = true)
3435
public class TrackingSessionService {
3536

37+
private static final String ACTIVE_SESSION_UNIQUE_INDEX = "uq_tracking_sessions_user_active";
38+
3639
private final TrackingSessionRepository trackingSessionRepository;
3740
private final MountainRepository mountainRepository;
3841
private final CourseRepository courseRepository;
@@ -153,7 +156,26 @@ private TrackingSession saveSession(TrackingSession session) {
153156
try {
154157
return trackingSessionRepository.save(session);
155158
} catch (DataIntegrityViolationException e) {
156-
throw new GeneralException(ErrorStatus.TRACKING_SESSION_ALREADY_IN_PROGRESS);
159+
if (isActiveSessionUniqueViolation(e)) {
160+
throw new GeneralException(ErrorStatus.TRACKING_SESSION_ALREADY_IN_PROGRESS);
161+
}
162+
throw e;
163+
}
164+
}
165+
166+
private boolean isActiveSessionUniqueViolation(Throwable exception) {
167+
Throwable current = exception;
168+
while (current != null) {
169+
if (current instanceof ConstraintViolationException constraintViolation
170+
&& ACTIVE_SESSION_UNIQUE_INDEX.equals(constraintViolation.getConstraintName())) {
171+
return true;
172+
}
173+
String message = current.getMessage();
174+
if (message != null && message.contains(ACTIVE_SESSION_UNIQUE_INDEX)) {
175+
return true;
176+
}
177+
current = current.getCause();
157178
}
179+
return false;
158180
}
159181
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
-- 유저당 활성 트래킹 세션(IN_PROGRESS/PAUSED)은 1개만 허용한다.
22
-- 애플리케이션의 exists -> save 검증만으로는 동시 생성 요청을 막을 수 없어서 DB 제약으로 보강한다.
3+
-- 기존 중복 데이터가 있으면 사용자별 최신 활성 세션 1건만 유지하고 나머지는 ABANDONED 처리한다.
4+
WITH ranked_active_sessions AS (
5+
SELECT
6+
id,
7+
ROW_NUMBER() OVER (
8+
PARTITION BY user_id
9+
ORDER BY started_at DESC, id DESC
10+
) AS row_number
11+
FROM tracking_sessions
12+
WHERE status IN ('IN_PROGRESS', 'PAUSED')
13+
)
14+
UPDATE tracking_sessions ts
15+
SET status = 'ABANDONED',
16+
ended_at = COALESCE(ts.ended_at, now()),
17+
updated_at = now()
18+
FROM ranked_active_sessions ras
19+
WHERE ts.id = ras.id
20+
AND ras.row_number > 1;
21+
322
CREATE UNIQUE INDEX IF NOT EXISTS uq_tracking_sessions_user_active
423
ON tracking_sessions (user_id)
524
WHERE status IN ('IN_PROGRESS', 'PAUSED');

0 commit comments

Comments
 (0)