Skip to content

Commit

Permalink
[Feature] - 여행 계획 상세 조회 API 구현 (#56)
Browse files Browse the repository at this point in the history
* fix: TravelPlanController Swagger label 여행기 -> 여행 계획으로 변경

* fix: PlanPlaceCreateRequest 장소명과 설명 예시 바뀐 곳 수정

* feat: 여행 계획 상세 조회 API 구현

* feat: 여행 계획 상세 조회 swagger 설명 추가

* feat: TravelPlanController Swagger에 Error 응답 추가

* feat: GlobalExceptionHandler 추가

* feat: TravelPlan 응답에서 날짜, 장소 순서별로 정렬 추가

* fix: TravelPlanResponse에 TravelPlan id 추가

* test: TravelPlanController 테스트 추가

* test: TravelPlanService 테스트 추가
  • Loading branch information
eunjungL authored Jul 19, 2024
1 parent 37c496b commit 062c37e
Show file tree
Hide file tree
Showing 17 changed files with 510 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package woowacourse.touroot.global.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import woowacourse.touroot.global.exception.dto.ExceptionResponse;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ExceptionResponse> handleBadRequestException(BadRequestException exception) {
log.info("BAD_REQUEST_EXCEPTION :: message = {}", exception.getMessage());
ExceptionResponse data = new ExceptionResponse(exception.getMessage());
return ResponseEntity.badRequest()
.body(data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package woowacourse.touroot.global.exception.dto;

public record ExceptionResponse(String message) {
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,62 @@
package woowacourse.touroot.travelplan.controller;

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.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest;
import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse;
import org.springframework.web.bind.annotation.*;
import woowacourse.touroot.global.exception.dto.ExceptionResponse;
import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest;
import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse;
import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse;
import woowacourse.touroot.travelplan.service.TravelPlanService;

@Tag(name = "여행기")
@Tag(name = "여행 계획")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/travel-plans")
public class TravelPlanController {

private final TravelPlanService travelPlanService;

@Operation(summary = "여행기 생성")
@Operation(
summary = "여행 계획 생성",
responses = {
@ApiResponse(
responseCode = "400",
description = "Body에 유효하지 않은 값이 존재하거나 지난 날짜에 대한 계획을 생성할 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
)
}
)
@PostMapping
public ResponseEntity<TravelPlanCreateResponse> createTravelPlan(@Valid @RequestBody TravelPlanCreateRequest request) {
public ResponseEntity<TravelPlanCreateResponse> createTravelPlan(
@Valid @RequestBody TravelPlanCreateRequest request
) {
TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request);
return ResponseEntity.ok()
.body(data);
return ResponseEntity.ok(data);
}

@Operation(
summary = "여행 계획 상세 조회",
responses = {
@ApiResponse(
responseCode = "400",
description = "존재하지 않은 여행 계획을 조회할 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
)
}
)
@GetMapping("/{id}")
public ResponseEntity<TravelPlanResponse> readTravelPlan(
@Parameter(description = "여행 계획 id") @PathVariable Long id
) {
TravelPlanResponse data = travelPlanService.readTravelPlan(id);
return ResponseEntity.ok(data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;

import java.time.LocalDate;
import java.util.List;

@Getter
Expand All @@ -32,4 +33,9 @@ public class TravelPlanDay extends BaseEntity {
public TravelPlanDay(int order, TravelPlan plan) {
this(null, order, plan, null);
}

public LocalDate getCurrentDate() {
LocalDate startDate = plan.getStartDate();
return startDate.plusDays(order);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package woowacourse.touroot.travelplan.dto;
package woowacourse.touroot.travelplan.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package woowacourse.touroot.travelplan.dto;
package woowacourse.touroot.travelplan.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package woowacourse.touroot.travelplan.dto;
package woowacourse.touroot.travelplan.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import woowacourse.touroot.place.domain.Place;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;
import woowacourse.touroot.travelplan.domain.TravelPlanPlace;

@Builder
public record PlanPlaceCreateRequest(
@Schema(description = "여행 장소 이름", example = "신나는 여행 장소")
@Schema(description = "여행 장소 이름", example = "잠실한강공원")
@NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName,
@Schema(description = "여행 장소 설명", example = "잠실한강공원")
@Schema(description = "여행 장소 설명", example = "신나는 여행 장소")
String description,
@Schema(description = "여행 장소 순서", example = "1")
@NotNull
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package woowacourse.touroot.travelplan.dto;
package woowacourse.touroot.travelplan.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import woowacourse.touroot.travelplan.domain.TravelPlan;

import java.time.LocalDate;
import java.util.List;

@Builder
public record TravelPlanCreateRequest(
@Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행")
@NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package woowacourse.touroot.travelplan.dto;
package woowacourse.touroot.travelplan.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package woowacourse.touroot.travelplan.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;

import java.time.LocalDate;
import java.util.List;

@Builder
public record TravelPlanDayResponse(
@Schema(description = "여행 일자") LocalDate date,
@Schema(description = "여행 장소별 정보") List<TravelPlanPlaceResponse> places
) {

public static TravelPlanDayResponse of(
TravelPlanDay planDay,
List<TravelPlanPlaceResponse> places
) {
return TravelPlanDayResponse.builder()
.date(planDay.getCurrentDate())
.places(places)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package woowacourse.touroot.travelplan.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import woowacourse.touroot.place.domain.Place;

@Builder
public record TravelPlanLocationResponse(
@Schema(description = "여행 장소 위도") String lat,
@Schema(description = "여행 계획 경도") String lng
) {

public static TravelPlanLocationResponse from(Place place) {
return TravelPlanLocationResponse.builder()
.lat(place.getLatitude())
.lng(place.getLongitude())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package woowacourse.touroot.travelplan.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import woowacourse.touroot.place.domain.Place;
import woowacourse.touroot.travelplan.domain.TravelPlanPlace;

@Builder
public record TravelPlanPlaceResponse(
@Schema(description = "여행 장소 이름") String placeName,
@Schema(description = "여행 장소 위치") TravelPlanLocationResponse location,
@Schema(description = "여행 장소 설명") String description
) {

public static TravelPlanPlaceResponse from(TravelPlanPlace planPlace) {
Place place = planPlace.getPlace();
TravelPlanLocationResponse locationResponse = TravelPlanLocationResponse.from(place);

return TravelPlanPlaceResponse.builder()
.placeName(place.getName())
.location(locationResponse)
.description(planPlace.getDescription())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package woowacourse.touroot.travelplan.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import woowacourse.touroot.travelplan.domain.TravelPlan;

import java.time.LocalDate;
import java.util.List;

@Builder
public record TravelPlanResponse(
@Schema(description = "여행 계획 id") Long id,
@Schema(description = "여행 계획 제목") String title,
@Schema(description = "여행 시작일") LocalDate startDate,
@Schema(description = "여행 계획 날짜별 정보") List<TravelPlanDayResponse> days
) {

public static TravelPlanResponse of(TravelPlan travelPlan, List<TravelPlanDayResponse> days) {
return TravelPlanResponse.builder()
.id(travelPlan.getId())
.title(travelPlan.getTitle())
.startDate(travelPlan.getStartDate())
.days(days)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import woowacourse.touroot.global.exception.BadRequestException;
import woowacourse.touroot.place.domain.Place;
import woowacourse.touroot.place.repository.PlaceRepository;
import woowacourse.touroot.travelplan.domain.TravelPlan;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;
import woowacourse.touroot.travelplan.dto.PlanDayCreateRequest;
import woowacourse.touroot.travelplan.dto.PlanPlaceCreateRequest;
import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest;
import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse;
import woowacourse.touroot.travelplan.domain.TravelPlanPlace;
import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest;
import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest;
import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest;
import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse;
import woowacourse.touroot.travelplan.dto.response.TravelPlanDayResponse;
import woowacourse.touroot.travelplan.dto.response.TravelPlanPlaceResponse;
import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse;
import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository;
import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository;
import woowacourse.touroot.travelplan.repository.TravelPlanRepository;

import java.util.Comparator;
import java.util.List;

@RequiredArgsConstructor
Expand All @@ -39,13 +45,15 @@ public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request

private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTravelPlan) {
for (PlanDayCreateRequest dayRequest : request.days()) {
// TODO: order는 배열 index로 변경
TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(savedTravelPlan));
createPlanPlace(dayRequest.places(), travelPlanDay);
}
}

private void createPlanPlace(List<PlanPlaceCreateRequest> request, TravelPlanDay travelPlanDay) {
for (PlanPlaceCreateRequest planRequest : request) {
// TODO: order는 배열 index로 변경
Place place = getPlace(planRequest);
travelPlanPlaceRepository.save(planRequest.toPlanPlace(travelPlanDay, place));
}
Expand All @@ -58,4 +66,29 @@ private Place getPlace(PlanPlaceCreateRequest planRequest) {
planRequest.location().lng()
).orElseGet(() -> placeRepository.save(planRequest.toPlace()));
}

@Transactional(readOnly = true)
public TravelPlanResponse readTravelPlan(Long planId) {
TravelPlan travelPlan = getTravelPlanById(planId);
return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan));
}

private TravelPlan getTravelPlanById(Long planId) {
return travelPlanRepository.findById(planId)
.orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다."));
}

private List<TravelPlanDayResponse> getTravelPlanDayResponses(TravelPlan travelPlan) {
return travelPlan.getDays().stream()
.sorted(Comparator.comparing(TravelPlanDay::getOrder))
.map(day -> TravelPlanDayResponse.of(day, getTravelPlanPlaceResponses(day)))
.toList();
}

private List<TravelPlanPlaceResponse> getTravelPlanPlaceResponses(TravelPlanDay day) {
return day.getPlaces().stream()
.sorted(Comparator.comparing(TravelPlanPlace::getOrder))
.map(TravelPlanPlaceResponse::from)
.toList();
}
}
Loading

0 comments on commit 062c37e

Please sign in to comment.