Skip to content
2 changes: 1 addition & 1 deletion k8s/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ resources:
- minio/service.yaml
images:
- name: ghcr.io/semosan/semosan_be
newTag: 14e02fc87a6a73e49418a399891e97c2b839b3df
newTag: fb0eb2823ad7e4ee955bd3941443be46e88a22cf
2 changes: 2 additions & 0 deletions src/main/java/com/semosan/api/ApiApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableJpaAuditing
@EnableScheduling
@SpringBootApplication
@EnableConfigurationProperties(KakaoProperties.class)
public class ApiApplication {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.semosan.api.common.base.BaseStatus;
import com.semosan.api.common.status.ErrorStatus;
import com.semosan.api.common.response.ApiResponse;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
Expand All @@ -11,6 +12,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@Slf4j
Expand Down Expand Up @@ -39,6 +41,32 @@ public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(
return ApiResponse.error(ErrorStatus.BAD_REQUEST);
}

// @Validated 컨트롤러의 @RequestParam/@PathVariable 단순 타입 검증 실패 (Spring 6 MethodValidationInterceptor 경로)
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(
ConstraintViolationException e
) {
String message = e.getConstraintViolations().stream()
.findFirst()
.map(v -> v.getPropertyPath().toString() + ": " + v.getMessage())
.orElse(ErrorStatus.BAD_REQUEST.getMessage());
log.warn("[*] ConstraintViolationException : {}", message);
return ApiResponse.error(ErrorStatus.BAD_REQUEST, message);
}

// Spring 6.1+ HandlerMethodValidationException (컨트롤러 메서드 파라미터 검증 실패 표준 경로)
@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<ApiResponse<Void>> handleHandlerMethodValidation(
HandlerMethodValidationException e
) {
String message = e.getAllErrors().stream()
.findFirst()
.map(error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : ErrorStatus.BAD_REQUEST.getMessage())
.orElse(ErrorStatus.BAD_REQUEST.getMessage());
log.warn("[*] HandlerMethodValidationException : {}", message);
return ApiResponse.error(ErrorStatus.BAD_REQUEST, message);
}

// null 참조로 발생한 서버 오류를 500 에러로 응답
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ApiResponse<Void>> handleNullPointerException(
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/semosan/api/common/status/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ public enum ErrorStatus implements BaseStatus {
HIKING_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "HIKE_404_1", "등산 기록을 찾을 수 없습니다."),
HIKING_RECORD_FORBIDDEN(HttpStatus.FORBIDDEN, "HIKE_403_1", "본인이 참여한 등산 기록만 공유할 수 있습니다."),

/**
* Tracking (트래킹 세션)
*/
TRACKING_SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_1", "트래킹 세션을 찾을 수 없습니다."),
TRACKING_SESSION_FORBIDDEN(HttpStatus.FORBIDDEN, "TRK_403_1", "본인의 트래킹 세션만 조작할 수 있습니다."),
TRACKING_SESSION_ALREADY_IN_PROGRESS(HttpStatus.CONFLICT, "TRK_409_1", "이미 진행 중인 트래킹 세션이 있습니다."),
TRACKING_SESSION_INVALID_STATE(HttpStatus.CONFLICT, "TRK_409_2", "현재 상태에서는 수행할 수 없는 작업입니다."),
TRACKING_COURSE_MOUNTAIN_MISMATCH(HttpStatus.BAD_REQUEST, "TRK_400_1", "선택한 코스가 해당 산의 코스가 아닙니다."),
TRACKING_COURSE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "TRK_400_2", "자유 기록이 아니면 코스 ID는 필수입니다."),
COURSE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_3", "코스를 찾을 수 없습니다."),

/**
* Post (게시글 공통)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,16 @@ public enum SuccessStatus implements BaseStatus {
GET_HIKING_RECORD_LIST_BY_MOUNTAIN_SUCCESS(HttpStatus.OK, "HIKING_200_4", "특정 산의 나의 등산 기록 목록 조회에 성공했습니다."),

/**
* Tracking
* Tracking — 진입 화면 (#45) & 세션 (#18)
*/
TRACKING_NEAREST_MOUNTAIN_SUCCESS(HttpStatus.OK, "TRK_200_1", "현재 위치 기준 가까운 산과 코스 조회에 성공했습니다."),
TRACKING_SESSION_CREATE_SUCCESS(HttpStatus.CREATED, "TRK_201_1", "트래킹 세션이 시작되었습니다."),
TRACKING_SESSION_GET_ACTIVE_SUCCESS(HttpStatus.OK, "TRK_200_2", "현재 진행 중인 트래킹 세션 조회에 성공했습니다."),
TRACKING_SESSION_GET_SUCCESS(HttpStatus.OK, "TRK_200_3", "트래킹 세션 상세 조회에 성공했습니다."),
TRACKING_SESSION_PAUSE_SUCCESS(HttpStatus.OK, "TRK_200_4", "트래킹 세션을 일시정지했습니다."),
TRACKING_SESSION_RESUME_SUCCESS(HttpStatus.OK, "TRK_200_5", "트래킹 세션을 재개했습니다."),
TRACKING_SESSION_COMPLETE_SUCCESS(HttpStatus.OK, "TRK_200_6", "트래킹 세션을 종료했습니다."),
TRACKING_SESSION_ABANDON_SUCCESS(HttpStatus.OK, "TRK_200_7", "트래킹 세션을 포기 처리했습니다."),

/**
* Image
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import com.semosan.api.domain.tracking.controller.docs.TrackingControllerDocs;
import com.semosan.api.domain.tracking.dto.response.NearbyMountainResponse;
import com.semosan.api.domain.tracking.service.TrackingService;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -16,21 +19,23 @@
@RestController
@RequestMapping("/api/tracking")
@RequiredArgsConstructor
@Validated
public class TrackingController implements TrackingControllerDocs {

private final TrackingService trackingService;

/**
* 트래킹 진입 화면에서 호출.
* 프론트가 디바이스 GPS 좌표(lat, lng)를 보내면, 가장 가까운 산과 그 산의 코스 목록을 반환한다.
* 인증 필수 — 트래킹은 로그인 사용자만 시작 가능.
* 인증 필수 — 트래킹은 로그인 사용자만 시작 가능 (userId 는 Spring Security 인증 통과 강제용).
* 사용자별 개인화는 추후 도입 시 service 로 userId 를 넘겨 활용.
*/
@GetMapping("/nearby-mountain")
@Override
public ResponseEntity<ApiResponse<NearbyMountainResponse>> getNearbyMountain(
@AuthenticationPrincipal Long userId,
@RequestParam Double lat,
@RequestParam Double lng
@RequestParam @Min(-90) @Max(90) Double lat,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GeneralExceptionAdvice에는 MethodArgumentNotValidException 처리는 있지만, @RequestParam @Min/@Max에서 발생할 수 있는 ConstraintViolationException 또는HandlerMethodValidationException 처리가 안 되어 있어서 이 경우 Exception 핸들러로 빠져서 잘못된 좌표 요청이 500으로 응답될 수 있을 듯합니당
한 번 체크해봐야할듯!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다 ! 수정할게여

@RequestParam @Min(-180) @Max(180) Double lng
) {
NearbyMountainResponse response = trackingService.getNearbyMountain(userId, lat, lng);
return ApiResponse.success(SuccessStatus.TRACKING_NEAREST_MOUNTAIN_SUCCESS, response);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.semosan.api.domain.tracking.controller;

import com.semosan.api.common.response.ApiResponse;
import com.semosan.api.common.status.SuccessStatus;
import com.semosan.api.domain.tracking.controller.docs.TrackingSessionControllerDocs;
import com.semosan.api.domain.tracking.dto.request.CreateTrackingSessionRequest;
import com.semosan.api.domain.tracking.dto.response.TrackingSessionResponse;
import com.semosan.api.domain.tracking.service.TrackingSessionService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/tracking/sessions")
@RequiredArgsConstructor
public class TrackingSessionController implements TrackingSessionControllerDocs {

private final TrackingSessionService trackingSessionService;

@PostMapping
@Override
public ResponseEntity<ApiResponse<TrackingSessionResponse>> createSession(
@AuthenticationPrincipal Long userId,
@Valid @RequestBody CreateTrackingSessionRequest request
) {
TrackingSessionResponse response = trackingSessionService.create(userId, request);
return ApiResponse.success(SuccessStatus.TRACKING_SESSION_CREATE_SUCCESS, response);
}

/**
* 앱 재진입 시 호출. 진행 중 세션이 없으면 data 가 비어 응답된다 (NON_NULL 직렬화).
*/
@GetMapping("/me/active")
@Override
public ResponseEntity<ApiResponse<TrackingSessionResponse>> getActiveSession(
@AuthenticationPrincipal Long userId
) {
TrackingSessionResponse response = trackingSessionService.getActive(userId).orElse(null);
return ApiResponse.success(SuccessStatus.TRACKING_SESSION_GET_ACTIVE_SUCCESS, response);
}

@GetMapping("/{sessionId}")
@Override
public ResponseEntity<ApiResponse<TrackingSessionResponse>> getSession(
@AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
) {
TrackingSessionResponse response = trackingSessionService.get(userId, sessionId);
return ApiResponse.success(SuccessStatus.TRACKING_SESSION_GET_SUCCESS, response);
}

@PostMapping("/{sessionId}/pause")
@Override
public ResponseEntity<ApiResponse<TrackingSessionResponse>> pauseSession(
@AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
) {
TrackingSessionResponse response = trackingSessionService.pause(userId, sessionId);
return ApiResponse.success(SuccessStatus.TRACKING_SESSION_PAUSE_SUCCESS, response);
}

@PostMapping("/{sessionId}/resume")
@Override
public ResponseEntity<ApiResponse<TrackingSessionResponse>> resumeSession(
@AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
) {
TrackingSessionResponse response = trackingSessionService.resume(userId, sessionId);
return ApiResponse.success(SuccessStatus.TRACKING_SESSION_RESUME_SUCCESS, response);
}

@PostMapping("/{sessionId}/complete")
@Override
public ResponseEntity<ApiResponse<TrackingSessionResponse>> completeSession(
@AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
) {
TrackingSessionResponse response = trackingSessionService.complete(userId, sessionId);
return ApiResponse.success(SuccessStatus.TRACKING_SESSION_COMPLETE_SUCCESS, response);
}

@PostMapping("/{sessionId}/abandon")
@Override
public ResponseEntity<ApiResponse<TrackingSessionResponse>> abandonSession(
@AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
) {
TrackingSessionResponse response = trackingSessionService.abandon(userId, sessionId);
return ApiResponse.success(SuccessStatus.TRACKING_SESSION_ABANDON_SUCCESS, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.semosan.api.domain.tracking.controller.docs;

import com.semosan.api.common.response.ApiResponse;
import com.semosan.api.domain.tracking.dto.request.CreateTrackingSessionRequest;
import com.semosan.api.domain.tracking.dto.response.TrackingSessionResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "Tracking Session", description = "트래킹 세션 관리 API")
public interface TrackingSessionControllerDocs {

@Operation(
summary = "트래킹 세션 시작",
description = "산/코스 정보로 새 트래킹 세션을 생성합니다. 자유 기록(isFreeRecording=true)이면 courseId 는 무시됩니다. "
+ "유저당 진행 중 세션이 이미 있으면 409 응답."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "세션 생성 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "산 또는 코스 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 진행 중인 세션 존재",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
ResponseEntity<ApiResponse<TrackingSessionResponse>> createSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@Valid @RequestBody CreateTrackingSessionRequest request
);

@Operation(summary = "현재 진행 중 트래킹 세션 조회",
description = "앱 재진입 시 호출. 진행 중(IN_PROGRESS/PAUSED) 세션이 없으면 data 가 비어 응답됩니다.")
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공")})
ResponseEntity<ApiResponse<TrackingSessionResponse>> getActiveSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId
);

@Operation(summary = "트래킹 세션 상세 조회", description = "본인 소유 세션만 조회 가능합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "본인 세션 아님",
content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "세션 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
ResponseEntity<ApiResponse<TrackingSessionResponse>> getSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@Parameter(description = "세션 ID", required = true) @PathVariable Long sessionId
);

@Operation(summary = "트래킹 세션 일시정지", description = "IN_PROGRESS 상태에서만 가능. 점 수집은 멈추고 duration 에서 제외됩니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "일시정지 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "현재 상태에서 일시정지 불가",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
ResponseEntity<ApiResponse<TrackingSessionResponse>> pauseSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
);

@Operation(summary = "트래킹 세션 재개", description = "PAUSED 상태에서만 가능. 일시정지 누적 시간이 합산됩니다.")
ResponseEntity<ApiResponse<TrackingSessionResponse>> resumeSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
);

@Operation(summary = "트래킹 세션 정상 종료",
description = "세션 상태/시각만 마감합니다. 실제 HikingRecord 변환은 #20 에서 구현됩니다.")
ResponseEntity<ApiResponse<TrackingSessionResponse>> completeSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
);

@Operation(summary = "트래킹 세션 포기", description = "기록 없이 세션을 ABANDONED 처리합니다.")
ResponseEntity<ApiResponse<TrackingSessionResponse>> abandonSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
);
Comment on lines +69 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Complete API documentation with error response codes.

The resumeSession, completeSession, and abandonSession methods are missing @ApiResponses annotations, while earlier methods in the interface consistently document their response codes. These mutation operations can produce error responses (404 for missing session, 403 for ownership violation, 409 for invalid state transitions), and API consumers need this information to implement proper error handling.

📝 Add `@ApiResponses` annotations for consistency
 `@Operation`(summary = "트래킹 세션 재개", description = "PAUSED 상태에서만 가능. 일시정지 누적 시간이 합산됩니다.")
+@ApiResponses({
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "200", description = "재개 성공"),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "403", description = "본인 세션 아님",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "404", description = "세션 없음",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "409", description = "현재 상태에서 재개 불가",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class)))
+})
 ResponseEntity<ApiResponse<TrackingSessionResponse>> resumeSession(
         `@Parameter`(hidden = true) `@AuthenticationPrincipal` Long userId,
         `@PathVariable` Long sessionId
 );

 `@Operation`(summary = "트래킹 세션 정상 종료",
         description = "세션 상태/시각만 마감합니다. 실제 HikingRecord 변환은 `#20` 에서 구현됩니다.")
+@ApiResponses({
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "200", description = "종료 성공"),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "403", description = "본인 세션 아님",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "404", description = "세션 없음",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "409", description = "현재 상태에서 종료 불가",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class)))
+})
 ResponseEntity<ApiResponse<TrackingSessionResponse>> completeSession(
         `@Parameter`(hidden = true) `@AuthenticationPrincipal` Long userId,
         `@PathVariable` Long sessionId
 );

 `@Operation`(summary = "트래킹 세션 포기", description = "기록 없이 세션을 ABANDONED 처리합니다.")
+@ApiResponses({
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "200", description = "포기 성공"),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "403", description = "본인 세션 아님",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
+        `@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "404", description = "세션 없음",
+                content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class)))
+})
 ResponseEntity<ApiResponse<TrackingSessionResponse>> abandonSession(
📝 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.

Suggested change
@Operation(summary = "트래킹 세션 재개", description = "PAUSED 상태에서만 가능. 일시정지 누적 시간이 합산됩니다.")
ResponseEntity<ApiResponse<TrackingSessionResponse>> resumeSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
);
@Operation(summary = "트래킹 세션 정상 종료",
description = "세션 상태/시각만 마감합니다. 실제 HikingRecord 변환은 #20 에서 구현됩니다.")
ResponseEntity<ApiResponse<TrackingSessionResponse>> completeSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
);
@Operation(summary = "트래킹 세션 포기", description = "기록 없이 세션을 ABANDONED 처리합니다.")
ResponseEntity<ApiResponse<TrackingSessionResponse>> abandonSession(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@PathVariable Long sessionId
);
`@Operation`(summary = "트래킹 세션 재개", description = "PAUSED 상태에서만 가능. 일시정지 누적 시간이 합산됩니다.")
`@ApiResponses`({
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "200", description = "재개 성공"),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "403", description = "본인 세션 아님",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "404", description = "세션 없음",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "409", description = "현재 상태에서 재개 불가",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class)))
})
ResponseEntity<ApiResponse<TrackingSessionResponse>> resumeSession(
`@Parameter`(hidden = true) `@AuthenticationPrincipal` Long userId,
`@PathVariable` Long sessionId
);
`@Operation`(summary = "트래킹 세션 정상 종료",
description = "세션 상태/시각만 마감합니다. 실제 HikingRecord 변환은 `#20` 에서 구현됩니다.")
`@ApiResponses`({
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "200", description = "종료 성공"),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "403", description = "본인 세션 아님",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "404", description = "세션 없음",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "409", description = "현재 상태에서 종료 불가",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class)))
})
ResponseEntity<ApiResponse<TrackingSessionResponse>> completeSession(
`@Parameter`(hidden = true) `@AuthenticationPrincipal` Long userId,
`@PathVariable` Long sessionId
);
`@Operation`(summary = "트래킹 세션 포기", description = "기록 없이 세션을 ABANDONED 처리합니다.")
`@ApiResponses`({
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "200", description = "포기 성공"),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "403", description = "본인 세션 아님",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class))),
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "404", description = "세션 없음",
content = `@Content`(schema = `@Schema`(implementation = ApiResponse.class)))
})
ResponseEntity<ApiResponse<TrackingSessionResponse>> abandonSession(
`@Parameter`(hidden = true) `@AuthenticationPrincipal` Long userId,
`@PathVariable` Long sessionId
);
🤖 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/semosan/api/domain/tracking/controller/docs/TrackingSessionControllerDocs.java`
around lines 69 - 86, Add consistent `@ApiResponses` annotations to the
TrackingSessionControllerDocs interface for the resumeSession, completeSession,
and abandonSession methods: annotate each method (resumeSession,
completeSession, abandonSession) with `@ApiResponses` listing the success response
(200 with TrackingSessionResponse) and the possible error responses 404 (session
not found), 403 (not the owner), and 409 (invalid state transition), matching
the format used by earlier methods in this interface so API docs show these
error codes.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.semosan.api.domain.tracking.dto.request;

import jakarta.validation.constraints.NotNull;

public record CreateTrackingSessionRequest(
@NotNull(message = "산 ID는 필수입니다.")
Long mountainId,

/** 자유 기록(코스 미선택) 인 경우 null */
Long courseId,

@NotNull(message = "자유 기록 여부는 필수입니다.")
Boolean isFreeRecording
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.semosan.api.domain.tracking.dto.response;

import com.semosan.api.domain.tracking.entity.TrackingSession;
import com.semosan.api.domain.tracking.enums.TrackingSessionStatus;

import java.time.LocalDateTime;

public record TrackingSessionResponse(
Long sessionId,
Long userId,
Long mountainId,
String mountainName,
Long courseId,
String courseName,
Boolean isFreeRecording,
TrackingSessionStatus status,
LocalDateTime startedAt,
LocalDateTime endedAt,
LocalDateTime pausedAt,
Integer pausedSecondsTotal
) {

public static TrackingSessionResponse from(TrackingSession session) {
return new TrackingSessionResponse(
session.getId(),
session.getUser().getId(),
session.getMountain().getId(),
session.getMountain().getName(),
session.getCourse() != null ? session.getCourse().getId() : null,
session.getCourse() != null ? session.getCourse().getName() : null,
session.getIsFreeRecording(),
session.getStatus(),
session.getStartedAt(),
session.getEndedAt(),
session.getPausedAt(),
session.getPausedSecondsTotal()
);
}
}
Loading