Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -35,7 +36,14 @@ public enum CourseErrorCode implements BaseErrorCode {
RECOMMENDATION_ALREADY_LIKED(HttpStatus.CONFLICT, "RECOMMENDATION_ALREADY_LIKED", "이미 좋아요를 누른 추천입니다."),
DUPLICATE_PLACE(HttpStatus.CONFLICT, "DUPLICATE_PLACE", "이미 코스에 추가된 장소입니다."),
PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "PLACE_NOT_FOUND", "장소를 찾을 수 없습니다."),
DUPLICATE_RECOMMENDATION(HttpStatus.CONFLICT, "DUPLICATE_RECOMMENDATION", "이미 추천된 장소입니다.");
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", "코스에 속하지 않는 장소가 포함되어 있습니다.");

private final HttpStatus status;
private final String code;
Expand Down
35 changes: 16 additions & 19 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,26 @@ 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);
}

// 마지막 장소 순서 구하기 (없으면 0)
Integer maxOrder = coursePlaceRepository.findMaxPlaceOrderByCourse_CourseId(courseId).orElse(0);
CoursePlace coursePlace = appendPlaceToCourse(course, recommendation.getPlace(), userId);

// 추가자(User) 조회
User addedByUser = userService.getActiveUser(userId);
return new CourseResponse.CoursePlaceAdded(coursePlace.getCoursePlaceId());
}

// Builder를 이용한 CoursePlace 생성
CoursePlace coursePlace = CoursePlace.builder()
// 코스 맨 뒤에 장소 추가 (추천담기/투데이추가 공통): 중복확인 + 마지막순서+1 + CoursePlace 생성
// 투데이 장소추가(TDY-006)는 TodayCourseService 가 이 메서드를 재사용한다.
public CoursePlace appendPlaceToCourse(Course course, Place place, Long userId) {
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