Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@

import static com.server.running_handai.global.response.ResponseCode.*;

import com.server.running_handai.domain.course.dto.CourseCreateRequestDto;
import com.server.running_handai.domain.course.dto.CourseDetailDto;
import com.server.running_handai.domain.course.dto.CourseFilterRequestDto;
import com.server.running_handai.domain.course.dto.CourseInfoWithDetailsDto;
import com.server.running_handai.domain.course.dto.CourseSummaryDto;
import com.server.running_handai.domain.course.dto.GpxCourseRequestDto;
import com.server.running_handai.domain.course.dto.*;
import com.server.running_handai.domain.course.service.CourseService;
import com.server.running_handai.global.oauth.CustomOAuth2User;
import com.server.running_handai.global.response.CommonResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
Expand Down Expand Up @@ -97,6 +96,44 @@ public ResponseEntity<CommonResponse<CourseSummaryDto>> getCourseSummary(
return ResponseEntity.ok(CommonResponse.success(SUCCESS, courseSummary));
}

@Operation(summary = "GPX 파일 다운로드", description = "GPX 파일을 다운로드할 수 있는 Presigned GET URL을 발급합니다. 해당 URL의 유효시간은 1시간입니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공 - SUCCESS"),
@ApiResponse(responseCode = "401", description = "토큰 인증 필요 - UNAUTHORIZED_ACCESS"),
@ApiResponse(responseCode = "403", description = "실패 (해당 사용자가 만든 코스가 아님) - NOT_COURSE_CREATOR"),
@ApiResponse(responseCode = "404", description = "실패 (존재하지 않는 코스) - COURSE_NOT_FOUND")
})
@GetMapping("/api/members/me/courses/{courseId}/gpx")
public ResponseEntity<CommonResponse<GpxPathDto>> downloadGpx(
@Parameter(description = "다운로드하려는 코스 ID", required = true)
@PathVariable("courseId") Long courseId,
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
Long memberId = customOAuth2User.getMember().getId();
log.info("[코스 GPX 다운로드] courseId: {}, memberId: {}", courseId, memberId);
GpxPathDto gpxPath = courseService.downloadGpx(courseId, memberId);
return ResponseEntity.ok(CommonResponse.success(SUCCESS, gpxPath));
}

@Operation(summary = "내 코스 전체 조회", description = "사용자가 생성한 코스 목록을 정렬 조건에 따라 조회합니다. 정렬 조건은 최신순, 오래된순, 짧은 거리순, 긴 거리순으로 총 4개입니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공 - SUCCESS"),
@ApiResponse(responseCode = "401", description = "토큰 인증 필요 - UNAUTHORIZED_ACCESS")
})
@GetMapping("/api/members/me/courses")
public ResponseEntity<CommonResponse<MyCourseDetailDto>> getMyCourses(
@Parameter(
description = "정렬 조건",
schema = @Schema(allowableValues = {"latest", "oldest", "short", "long"})
)
@RequestParam(defaultValue = "latest") String sortBy,
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
Long memberId = customOAuth2User.getMember().getId();
MyCourseDetailDto myCourseDetail = courseService.getMyCourses(memberId, sortBy);
return ResponseEntity.ok(CommonResponse.success(SUCCESS, myCourseDetail));
}

@Operation(summary = "지역 판별", description = "특정 위치 좌표가 부산 내 지역인지 판별합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공")
Expand Down Expand Up @@ -145,5 +182,4 @@ public ResponseEntity<CommonResponse<Void>> deleteMemberCourse(
courseService.deleteMemberCourse(memberId, courseId);
return ResponseEntity.ok(CommonResponse.success(SUCCESS_COURSE_REMOVE, null));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.server.running_handai.domain.course.dto;

public record GpxPathDto(
Long courseId,
String gpxPath
) {
public static GpxPathDto from(Long courseId, String gpxPath) {
return new GpxPathDto(
courseId,
gpxPath
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.server.running_handai.domain.course.dto;

import java.util.List;

public record MyCourseDetailDto(
int courseCount,
List<CourseInfoDto> courses
) {
public static MyCourseDetailDto from(List<CourseInfoDto> courseInfoDtos) {
return new MyCourseDetailDto(
courseInfoDtos.size(),
courseInfoDtos
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,7 @@
import com.server.running_handai.domain.spot.entity.CourseSpot;
import com.server.running_handai.domain.review.entity.Review;
import com.server.running_handai.global.entity.BaseTimeEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.server.running_handai.domain.course.entity.Course;
import java.util.List;
import java.util.Optional;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down Expand Up @@ -92,5 +94,26 @@ public interface CourseRepository extends JpaRepository<Course, Long> {
"WHERE c.id = :courseId")
Optional<Course> findCourseWithDetailsById(@Param("courseId") Long courseId);

/**
* Member가 생성한 Course 목록을 정렬 조건에 따라 조회
*/
@Query(
value = "SELECT " +
" c.course_id AS id, " +
" c.name, " +
" ci.img_url AS thumbnailUrl, " +
" c.distance, " +
" c.duration, " +
" c.max_ele AS maxElevation, " +
" 0.0 AS distanceFromUser " +
"FROM " +
" course c " +
"LEFT JOIN " +
" course_image ci ON c.course_id = ci.course_id " +
"WHERE c.member_id = :memberId ",
nativeQuery = true
)
List<CourseInfoDto> findMyCoursesBySort(@Param("memberId") Long memberId, Sort sort);

boolean existsByName(String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.server.running_handai.domain.bookmark.repository.BookmarkRepository;
import com.server.running_handai.domain.course.dto.CourseCreateRequestDto;
import com.server.running_handai.domain.course.dto.CourseSummaryDto;
import com.server.running_handai.domain.bookmark.dto.BookmarkCountDto;
import com.server.running_handai.domain.bookmark.dto.BookmarkInfoDto;
import com.server.running_handai.domain.course.dto.CourseDetailDto;
import com.server.running_handai.domain.course.dto.CourseFilterRequestDto;
import com.server.running_handai.domain.course.dto.CourseInfoDto;
import com.server.running_handai.domain.course.dto.CourseInfoWithDetailsDto;
import com.server.running_handai.domain.course.dto.GpxCourseRequestDto;
import com.server.running_handai.domain.course.dto.TrackPointDto;
import com.server.running_handai.domain.course.dto.*;
import com.server.running_handai.domain.course.entity.Course;
import com.server.running_handai.domain.course.entity.TrackPoint;
import com.server.running_handai.domain.course.event.CourseCreatedEvent;
Expand All @@ -31,7 +22,9 @@
import com.server.running_handai.domain.review.service.ReviewService;
import com.server.running_handai.domain.spot.dto.SpotInfoDto;
import com.server.running_handai.domain.spot.repository.SpotRepository;
import com.server.running_handai.global.response.ResponseCode;
import com.server.running_handai.global.response.exception.BusinessException;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand All @@ -45,6 +38,7 @@
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -65,6 +59,7 @@ public class CourseService {
private final MemberRepository memberRepository;
private final GeometryFactory geometryFactory;
private final ReviewService reviewService;
private final FileService fileService;
private final CourseDataService courseDataService;
private final FileService fileService;
private final KakaoMapService kakaoMapService;
Expand Down Expand Up @@ -236,6 +231,47 @@ public CourseSummaryDto getCourseSummary(Long courseId, Long memberId) {
return CourseSummaryDto.from(course, reviewCount, starAverage, reviewInfoDtos, spotInfoDtos);
}

/**
* 내가 생성한 코스의 GPX 파일 다운로드를 위한 Presigned GET URL을 발급합니다.
* 해당 URL의 유효시간은 1시간입니다.
*
* @param courseId 다운로드하려는 코스 ID
* @param memberId 다운로드 요청한 회원 ID
* @return GPX 파일 다운로드용 Presigned GET URL이 포함된 DTO
*/
public GpxPathDto downloadGpx(Long courseId, Long memberId) {
Course course = courseRepository.findById(courseId).orElseThrow(() -> new BusinessException(COURSE_NOT_FOUND));

// 해당 Course를 만든 Member가 아닌 경우
if (!course.getCreator().getId().equals(memberId)) {
throw new BusinessException(ResponseCode.NOT_COURSE_CREATOR);
}

// Presigned GET URL 발급 (1시간)
String gpxPath = fileService.getPresignedGetUrl(course.getGpxPath(), 60);

return GpxPathDto.from(courseId, gpxPath);
}

/**
* 사용자가 생성한 코스 목록을 정렬 조건에 따라 조회합니다.
*
* @param memberId 조회 요청한 회원 ID
* @param sortBy 정렬 조건 (latest, oldest, short, long)
* @return 정렬된 코스 목록이 포함된 DTO
*/
public MyCourseDetailDto getMyCourses(Long memberId, String sortBy) {
Sort sort = switch (sortBy) {
case "oldest" -> Sort.by("created_at").ascending();
case "short" -> Sort.by("distance").ascending();
case "long" -> Sort.by("distance").descending();
default -> Sort.by("created_at").descending();
};

List<CourseInfoDto> courseInfoDtos = courseRepository.findMyCoursesBySort(memberId, sort);
return MyCourseDetailDto.from(courseInfoDtos);
}

/**
* 주어진 좌표가 부산 내에 있는지 판별합니다.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum ResponseCode {

// FORBIDDEN (403)
ACCESS_DENIED(FORBIDDEN, "접근 권한이 없습니다."),
NOT_COURSE_CREATOR(FORBIDDEN, "해당 코스를 만든 사용자가 아닙니다."),
NO_AUTHORITY_TO_DELETE_COURSE(FORBIDDEN, "코스 삭제 권한이 없습니다."),

// NOT_FOUND (404)
Expand Down
Loading