Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] - 여행 계획 상세 조회 API 구현 #56

Merged
merged 11 commits into from
Jul 19, 2024
Merged
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);
}
Comment on lines +13 to +19
Copy link

Choose a reason for hiding this comment

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

예외 핸들러 확인했습니다.
다음에 작성할 일이 있으면 비슷한 포맷으로 저도 작성하도록 할게요!

Comment on lines +13 to +19
Copy link
Member

Choose a reason for hiding this comment

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

지금은 400 에러밖에 없지만, 이후 다양한 번호의 에러를 응답해 줘야 할때 조금 복잡해질수도 있을 것 같아요..!
예외 자체에 StatusCode를 두고, 이를 ResponseEntity가 받게하는 구조는 어떻게 생각하시나욥!!

Copy link
Author

Choose a reason for hiding this comment

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

예외 자체에 status code를 두는 방법 좋은 것 같습니다! 예외 메시지도 아예 둬버리면 더 통일되게 관리할 수도 있을 것 같네용
예외 관련해서 BE 회의 한 번 진행하시죠~! 👍

}
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) {
}
Comment on lines +1 to +4
Copy link

Choose a reason for hiding this comment

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

현재는 예외의 종류가 많지 않아서 이정도가 담백한 구현인 듯 해요!
다만 BadRequest의 종류가 다양해진다면 별도로 운용하는 에러 코드가 필요해질수도 있을 것 같다는 생각이에요 🤔
반영은 저희 나중에 생각하시죠..!
다만 생각은 하고 있으면 좋을 것 같아 코멘트 남겨놉니다 👍

Copy link
Author

Choose a reason for hiding this comment

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

에러 코드 도입하는건 저도 긍정적으로 생각합니다~!
이후 케이스가 많아졌을 떄 한 번 얘기해보죵

Copy link
Member

Choose a reason for hiding this comment

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

좀 더 표준화된 방식인 ProblemDetail을 사용하는 것에 대해서는 어떻게 생각하실까요?!

Copy link
Author

Choose a reason for hiding this comment

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

problem detail이 제공되는건 처음 알았네요~! 예외 리팩토링 진행 시 반영하겠습니다
굳굳 낙낙쓰

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) {

Choose a reason for hiding this comment

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

정적 팩토리 메소드 네이밍 👍

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
Loading