Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
AWS_S3_BUCKET=
AWS_REGION=
TOUR_SERVICE_KEY=
Original file line number Diff line number Diff line change
@@ -1,46 +1,72 @@
package com.daytodo.domain.course.controller;

import com.daytodo.domain.course.dto.request.CourseReqDTO;
import com.daytodo.domain.course.dto.response.CourseResDTO;
import com.daytodo.domain.course.dto.request.TodayCourseRequest;
import com.daytodo.domain.course.dto.response.TodayCourseResponse;
import com.daytodo.domain.course.service.TodayCourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Today Course")
@RestController
@RequiredArgsConstructor
@RequestMapping("/courses")
public class TodayCourseController {

private final TodayCourseService todayCourseService;

// TODO: JWT 인증 적용 후 @AuthenticationPrincipal 로 교체
@Operation(summary = "투데이 코스 조회")
@GetMapping("/today")
public CourseResDTO.GetTodayCourse getTodayCourse(
@RequestHeader("X-User-Id") Long userId
public TodayCourseResponse.GetTodayCourse getTodayCourse(
@AuthenticationPrincipal Long userId
) {
return todayCourseService.getTodayCourse(userId);
}

@Operation(summary = "투데이 코스 종료")
@PostMapping("/{courseId}/complete")
public CourseResDTO.CompleteCourse completeCourse(
@RequestHeader("X-User-Id") Long userId,
public TodayCourseResponse.CompleteCourse completeCourse(
@AuthenticationPrincipal Long userId,
@PathVariable Long courseId
) {
return todayCourseService.completeCourse(userId, courseId);
}

@Operation(summary = "투데이 추억 사진 저장")
@PostMapping("/{courseId}/photos")
public CourseResDTO.SaveMemoryPhotos saveMemoryPhotos(
@RequestHeader("X-User-Id") Long userId,
public TodayCourseResponse.SaveMemoryPhotos saveMemoryPhotos(
@AuthenticationPrincipal Long userId,
@PathVariable Long courseId,
@RequestBody CourseReqDTO.SaveMemoryPhotos request
@RequestBody TodayCourseRequest.SaveMemoryPhotos request
) {
return todayCourseService.saveMemoryPhotos(userId, courseId, request);
}

@Operation(summary = "투데이 장소 추가")
@PostMapping("/{courseId}/today-places")
public TodayCourseResponse.GetCoursePlaces addPlaceToCourse(
@AuthenticationPrincipal Long userId,
@PathVariable Long courseId,
@RequestBody TodayCourseRequest.AddPlace request
) {
return todayCourseService.addPlaceToCourse(userId, courseId, request);
}

@Operation(summary = "투데이 장소 순서 변경")
@PatchMapping("/{courseId}/today-places/order")
public TodayCourseResponse.GetCoursePlaces reorderCoursePlaces(
@AuthenticationPrincipal Long userId,
@PathVariable Long courseId,
@RequestBody TodayCourseRequest.ReorderCoursePlaces request
) {
return todayCourseService.reorderCoursePlaces(userId, courseId, request);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.daytodo.domain.course.converter;

import com.daytodo.domain.course.dto.response.CourseResDTO;
import com.daytodo.domain.course.dto.response.TodayCourseResponse;
import com.daytodo.domain.course.entity.Course;
import com.daytodo.domain.course.entity.MemoryPhoto;

Expand Down Expand Up @@ -30,16 +30,16 @@ public static List<MemoryPhoto> toMemoryPhotos(
.toList();
}

public static CourseResDTO.SaveMemoryPhotos toSaveMemoryPhotos(List<MemoryPhoto> memoryPhotos) {
List<CourseResDTO.SaveMemoryPhotos.PhotoItem> photos = memoryPhotos.stream()
public static TodayCourseResponse.SaveMemoryPhotos toSaveMemoryPhotos(List<MemoryPhoto> memoryPhotos) {
List<TodayCourseResponse.SaveMemoryPhotos.PhotoItem> photos = memoryPhotos.stream()
.map(MemoryPhotoConverter::toPhotoItem)
.toList();

return new CourseResDTO.SaveMemoryPhotos(photos.size(), photos);
return new TodayCourseResponse.SaveMemoryPhotos(photos.size(), photos);
}

private static CourseResDTO.SaveMemoryPhotos.PhotoItem toPhotoItem(MemoryPhoto memoryPhoto) {
return CourseResDTO.SaveMemoryPhotos.PhotoItem.builder()
private static TodayCourseResponse.SaveMemoryPhotos.PhotoItem toPhotoItem(MemoryPhoto memoryPhoto) {
return TodayCourseResponse.SaveMemoryPhotos.PhotoItem.builder()
.memoryPhotoId(memoryPhoto.getId())
.imageUrl(memoryPhoto.getImageUrl())
.photoOrder(memoryPhoto.getPhotoOrder())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.daytodo.domain.course.converter;

import com.daytodo.domain.course.dto.response.CourseResDTO;
import com.daytodo.domain.course.dto.response.TodayCourseResponse;
import com.daytodo.domain.course.entity.Course;
import com.daytodo.domain.course.entity.CourseMember;
import com.daytodo.domain.course.entity.CoursePlace;
Expand All @@ -17,17 +17,17 @@ public class TodayCourseConverter {
private TodayCourseConverter() {
}

public static CourseResDTO.GetTodayCourse toTodayCourse(
public static TodayCourseResponse.GetTodayCourse toTodayCourse(
Course course,
List<CourseMember> members,
List<CoursePlace> coursePlaces
) {
if (course == null) {
return new CourseResDTO.GetTodayCourse(null);
return new TodayCourseResponse.GetTodayCourse(null);
}

return new CourseResDTO.GetTodayCourse(
CourseResDTO.GetTodayCourse.TodayCourse.builder()
return new TodayCourseResponse.GetTodayCourse(
TodayCourseResponse.GetTodayCourse.TodayCourse.builder()
.courseId(course.getCourseId())
.courseName(course.getCourseName())
.members(members.stream().map(TodayCourseConverter::toMemberItem).toList())
Expand All @@ -36,27 +36,34 @@ public static CourseResDTO.GetTodayCourse toTodayCourse(
);
}

public static CourseResDTO.CompleteCourse toCompleteCourse(Course course) {
return CourseResDTO.CompleteCourse.builder()
// 코스 장소 추가/순서변경 응답: 장소 목록 -> places 응답
public static TodayCourseResponse.GetCoursePlaces toCoursePlaces(List<CoursePlace> coursePlaces) {
return new TodayCourseResponse.GetCoursePlaces(
coursePlaces.stream().map(TodayCourseConverter::toPlaceItem).toList()
);
}

public static TodayCourseResponse.CompleteCourse toCompleteCourse(Course course) {
return TodayCourseResponse.CompleteCourse.builder()
.courseId(course.getCourseId())
.courseStatus(course.getCourseStatus())
.completedAt(course.getCompletedAt())
.build();
}

private static CourseResDTO.GetTodayCourse.MemberItem toMemberItem(CourseMember courseMember) {
private static TodayCourseResponse.GetTodayCourse.MemberItem toMemberItem(CourseMember courseMember) {
User user = courseMember.getUser();

return CourseResDTO.GetTodayCourse.MemberItem.builder()
return TodayCourseResponse.GetTodayCourse.MemberItem.builder()
.nickname(user.getNickname())
.profileImageUrl(user.getProfileImageUrl())
.build();
}

private static CourseResDTO.GetTodayCourse.PlaceItem toPlaceItem(CoursePlace coursePlace) {
private static TodayCourseResponse.GetTodayCourse.PlaceItem toPlaceItem(CoursePlace coursePlace) {
Place place = coursePlace.getPlace();

return CourseResDTO.GetTodayCourse.PlaceItem.builder()
return TodayCourseResponse.GetTodayCourse.PlaceItem.builder()
.coursePlaceId(coursePlace.getCoursePlaceId())
.placeOrder(coursePlace.getPlaceOrder())
.placeName(place.getPlaceName())
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.daytodo.domain.course.dto.request;

import java.util.List;

public class TodayCourseRequest {

/*
* 추억 사진 저장
* 빈 목록/공백 URL 검증은 서비스에서 수행한다.
* (명세가 요구하는 INVALID_PARAMETER 코드로 응답하기 위해 Bean Validation 을 쓰지 않음)
*/
public record SaveMemoryPhotos(
List<String> imageUrls
){}

// 코스 장소 추가 : placeId 로 직접 추가. null 검증은 서비스에서 (INVALID_PARAMETER 코드 유지)
public record AddPlace(
Long placeId
){}

// 코스 장소 순서 변경 : 배열 순서대로 placeOrder 갱신. 검증은 서비스에서
public record ReorderCoursePlaces(
List<Long> orderedCoursePlaceIds
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import java.time.LocalDateTime;
import java.util.List;

public class CourseResDTO {
public class TodayCourseResponse {

// 투데이 코스 조회 (오늘 진행 중인 코스가 없으면 todayCourse 는 null)
public record GetTodayCourse(
Expand Down Expand Up @@ -35,6 +35,12 @@ public record PlaceItem(
){}
}

// 코스 장소 추가/순서변경 응답: 추가·변경 후 전체 목록을 순서대로 반환
// (투데이 조회의 장소 목록과 동일한 형태 재사용: coursePlaceId/placeOrder/placeName/category)
public record GetCoursePlaces(
List<GetTodayCourse.PlaceItem> places
){}

// 코스 종료
@Builder
public record CompleteCourse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ public class CoursePlace extends BaseCreatedEntity {
@Column(name = "visited_at")
private LocalDateTime visitedAt;


// 코스 장소 순서 변경
public void changeOrder(int placeOrder) {
this.placeOrder = placeOrder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ public enum CourseErrorCode implements BaseErrorCode {
PAST_COURSE_DATE(HttpStatus.BAD_REQUEST, "PAST_COURSE_DATE", "오늘 이전 날짜로 코스를 생성할 수 없습니다."),
INVALID_PRICE_RANGE(HttpStatus.BAD_REQUEST, "INVALID_PRICE_RANGE", "최대 금액은 최소 금액보다 작을 수 없습니다."),
INVITE_CODE_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "INVITE_CODE_GENERATION_FAILED", "초대코드를 생성하지 못했습니다."),
// 코스 종료(TDY-007)

// 코스 종료
INVALID_COURSE_STATUS(HttpStatus.CONFLICT, "INVALID_COURSE_STATUS", "진행 중인 코스만 종료할 수 있습니다."),

// 추억 사진 저장(TDY-008)
// 추억 사진 저장
EMPTY_MEMORY_PHOTO(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "저장할 이미지가 없습니다."),

COURSE_SAME_DAY_EDIT_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "COURSE_SAME_DAY_EDIT_NOT_ALLOWED", "당일 코스는 수정할 수 없습니다."),
Expand All @@ -36,6 +37,13 @@ public enum CourseErrorCode implements BaseErrorCode {
DUPLICATE_PLACE(HttpStatus.CONFLICT, "DUPLICATE_PLACE", "이미 코스에 추가된 장소입니다."),
PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "PLACE_NOT_FOUND", "장소를 찾을 수 없습니다."),
DUPLICATE_RECOMMENDATION(HttpStatus.CONFLICT, "DUPLICATE_RECOMMENDATION", "이미 추천된 장소입니다."),

// 코스 장소 추가
MISSING_PLACE_ID(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "placeId가 필요합니다."),

// 코스 장소 순서 변경
INVALID_PLACE_ORDER(HttpStatus.BAD_REQUEST, "INVALID_PARAMETER", "순서 정보가 올바르지 않습니다."),
COURSE_PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "COURSE_PLACE_NOT_FOUND", "코스에 속하지 않는 장소가 포함되어 있습니다.");
COURSE_RECOMMENDATION_FAILED(HttpStatus.CONFLICT, "COURSE_RECOMMENDATION_FAILED", "추천 코스를 생성할 수 없습니다.");

private final HttpStatus status;
Expand Down
38 changes: 20 additions & 18 deletions src/main/java/com/daytodo/domain/course/service/CourseService.java
Original file line number Diff line number Diff line change
Expand Up @@ -521,29 +521,31 @@ public CourseResponse.CoursePlaceAdded addRecommendationToCourse(Long courseId,
throw new ProjectException(CourseErrorCode.RECOMMENDATION_NOT_FOUND);
}

// 이미 코스에 추가된 장소인지 중복 확인
if (coursePlaceRepository.existsByCourse_CourseIdAndPlace_PlaceId(courseId, recommendation.getPlace().getPlaceId())) {
throw new ProjectException(CourseErrorCode.DUPLICATE_PLACE);
}
CoursePlace coursePlace = appendPlaceToCourse(course, recommendation.getPlace(), userId);

// 마지막 장소 순서 구하기 (없으면 0)
Integer maxOrder = coursePlaceRepository.findMaxPlaceOrderByCourse_CourseId(courseId).orElse(0);
return new CourseResponse.CoursePlaceAdded(coursePlace.getCoursePlaceId());
}

// 추가자(User) 조회
User addedByUser = userService.getActiveUser(userId);
// 코스 맨 뒤에 장소 추가 (추천담기/투데이추가 공통): 중복확인 + 마지막순서+1 + CoursePlace 생성
// 투데이 장소추가(TDY-006)는 TodayCourseService 가 이 메서드를 재사용한다.
public CoursePlace appendPlaceToCourse(Course course, Place place, Long userId) {
// 여러 요청이 동시에 장소를 추가해도 placeOrder 가 겹치지 않도록,
// 중복확인~순번계산~저장 구간을 코스 행 쓰기 락으로 직렬화한다. (추억 사진 저장과 동일 패턴)
courseRepository.findByIdForUpdate(course.getCourseId())
.orElseThrow(() -> new ProjectException(CourseErrorCode.COURSE_NOT_FOUND));

// Builder를 이용한 CoursePlace 생성
CoursePlace coursePlace = CoursePlace.builder()
if (coursePlaceRepository.existsByCourse_CourseIdAndPlace_PlaceId(course.getCourseId(), place.getPlaceId())) {
throw new ProjectException(CourseErrorCode.DUPLICATE_PLACE);
}
int nextOrder = coursePlaceRepository.findMaxPlaceOrderByCourse_CourseId(course.getCourseId()).orElse(0) + 1;
User addedBy = userService.getActiveUser(userId);
return coursePlaceRepository.save(CoursePlace.builder()
.course(course)
.place(recommendation.getPlace())
.addedBy(addedByUser)
.placeOrder(maxOrder + 1)
.place(place)
.addedBy(addedBy)
.placeOrder(nextOrder)
.coursePlaceStatus(CoursePlaceStatus.PENDING)
.build();

coursePlaceRepository.save(coursePlace);

return new CourseResponse.CoursePlaceAdded(coursePlace.getCoursePlaceId());
.build());
}
Comment thread
yxpjseo marked this conversation as resolved.

@Transactional
Expand Down
Loading