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 구현 #50

Merged
merged 7 commits into from
Jul 18, 2024
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
@@ -0,0 +1,8 @@
package woowacourse.touroot.global.exception;

public class BadRequestException extends RuntimeException {

public BadRequestException(String message) {
super(message);
}
Comment on lines +3 to +7
Copy link

Choose a reason for hiding this comment

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

cause와 message를 함께 받는 생성자도 열어두심이 어떨까요?
필요할 때 열 수도 있을 것 같지만 stack trace 타는 것이 예외의 기본적인 성질이라고 생각해서요..!

Copy link
Author

Choose a reason for hiding this comment

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

BadRequestException은 개발자가 직접 던지는 거라 cause를 추적할 일이 별로 없을 거라 생각합니다!
필요할 때 여는 방향으로 갈게용. 좋은 의견 👍

}
15 changes: 10 additions & 5 deletions backend/src/main/java/woowacourse/touroot/place/domain/Place.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package woowacourse.touroot.place.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class Place extends BaseEntity {

Expand All @@ -26,4 +27,8 @@ public class Place extends BaseEntity {
private String longitude;

private String googlePlaceId;

public Place(String name, String latitude, String longitude) {
this(null, name, latitude, longitude, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.place.domain.Place;

import java.util.Optional;

public interface PlaceRepository extends JpaRepository<Place, Long> {

Optional<Place> findByNameAndLatitudeAndLongitude(String name, String lat, String lng);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package woowacourse.touroot.travelplan.controller;

import io.swagger.v3.oas.annotations.Operation;
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 woowacourse.touroot.travelplan.service.TravelPlanService;

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

private final TravelPlanService travelPlanService;

@Operation(summary = "여행기 생성")
@PostMapping
public ResponseEntity<TravelPlanCreateResponse> createTravelPlan(@Valid @RequestBody TravelPlanCreateRequest request) {
TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request);
return ResponseEntity.ok()
.body(data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package woowacourse.touroot.travelplan.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;
import woowacourse.touroot.global.exception.BadRequestException;

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

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class TravelPlan extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String title;

@Column(nullable = false)
private LocalDate startDate;

@OneToMany(mappedBy = "plan")
private List<TravelPlanDay> days;

public TravelPlan(String title, LocalDate startDate) {
this(null, title, startDate, null);
}

public void validateStartDate() {
if (startDate.isBefore(LocalDate.now())) {
throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다.");
}
Comment on lines +36 to +40
Copy link

Choose a reason for hiding this comment

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

검증이 도메인 내에서 실행되고 있지 않고 서비스에서 호출되고 있군요.
검증 로직 자체가 서비스에 있다면 메서드의 이름과 특성을 바꾸는 것은 어떤가요.

  • ex ) boolean isPastTravelPlan()

이를 기반으로 서비스에서 반환값을 확인하고 예외 상황을 컨트롤 하는 것이 좋아보여요
서비스에서 검증 로직을 확인하려면 도메인으로 이동해야 하니 로직이 한 눈에 들어오지 않을 것 같습니다.
이름 상 도메인 내에서 검증을 하는 것인지 헷갈릴 것 같기도 하고요!

Copy link
Author

Choose a reason for hiding this comment

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

현구막이 미션 진행 중에 달아줬던 리뷰가 있었는데요, 서비스에서 요구사항이 변경됐을 때(Ex. 지난 날짜에 대한 정책이 지난 7일전과 같이 구체화 된다거나..) 가장 먼저 열어볼 곳은 domain이라는 리뷰가 있었습니다. 또한 평문적으로 읽었을 때도 travelPlan의 시작 날짜를 검증하는구나가 자연스럽게 읽히기 때문에 위의 형식을 사용했습니다. 좋은 제안 감사해용 리비~

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package woowacourse.touroot.travelplan.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;

import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class TravelPlanDay extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "plan_day_order", nullable = false)
Integer order;

@JoinColumn(name = "plan_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private TravelPlan plan;

@OneToMany(mappedBy = "day")
private List<TravelPlanPlace> places;

public TravelPlanDay(int order, TravelPlan plan) {
this(null, order, plan, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package woowacourse.touroot.travelplan.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import woowacourse.touroot.entity.BaseEntity;
import woowacourse.touroot.place.domain.Place;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class TravelPlanPlace extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String description;

@Column(name = "plan_place_order", nullable = false)
private Integer order;

@JoinColumn(name = "plan_day_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private TravelPlanDay day;

@JoinColumn(name = "place_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Place place;

public TravelPlanPlace(String description, int order, TravelPlanDay day, Place place) {
this(null, description, order, day, place);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package woowacourse.touroot.travelplan.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import woowacourse.touroot.travelplan.domain.TravelPlan;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;

import java.util.List;

public record PlanDayCreateRequest(
@Schema(description = "여행 계획 날짜", example = "1")
@NotNull(message = "날짜는 비어있을 수 없습니다.")
@Min(value = 0, message = "날짜는 1 이상이어야 합니다.")
int day,
@Schema(description = "여행 장소 정보")
@NotNull(message = "여행 장소 정보는 비어있을 수 없습니다.") List<PlanPlaceCreateRequest> places
Copy link
Member

Choose a reason for hiding this comment

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

PlanPlaceCreateRequest의 order 값들이 연속되지 않았는지 검증하는 로직이 추가되면 좋을 것 같습니다!
예를들어 1, 3, 4, 5 로 들어오는 경우요!

Copy link
Author

Choose a reason for hiding this comment

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

지금 order의 경우 값 자체가 중요하기 보다는 순서 보장을 위해 사용되고 있어서 연속된 숫자 검증에 대한 필요성이 잘 느껴지지 않습니다! 들어올 때마다 반복문을 사용해서 검증하는 만큼 리소스가 들텐데 최적화할 수 있는 방법을 좀 더 생각해보겠습니다 👍

) {

public TravelPlanDay toPlanDay(TravelPlan plan) {
return new TravelPlanDay(day, plan);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package woowacourse.touroot.travelplan.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record PlanLocationCreateRequest(
@Schema(description = "여행 장소 위도", example = "37.5175896")
@NotNull(message = "위도는 비어있을 수 없습니다.")
String lat,
@Schema(description = "여행 장소 경도", example = "127.0867236")
@NotNull(message = "경도는 비어있을 수 없습니다.")
String lng
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package woowacourse.touroot.travelplan.dto;

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

public record PlanPlaceCreateRequest(
@Schema(description = "여행 장소 이름", example = "신나는 여행 장소")
@NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName,
@Schema(description = "여행 장소 설명", example = "잠실한강공원")
String description,
@Schema(description = "여행 장소 순서", example = "1")
@NotNull
@Min(value = 0, message = "순서는 1 이상이어야 합니다.")
int order,
@NotNull PlanLocationCreateRequest location
) {

public TravelPlanPlace toPlanPlace(TravelPlanDay day, Place place) {
return new TravelPlanPlace(description, order, day, place);
}

public Place toPlace() {
return new Place(placeName, location.lat(), location.lng());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package woowacourse.touroot.travelplan.dto;

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

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

public record TravelPlanCreateRequest(
@Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행")
@NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.")
String title,
@Schema(description = "여행 계획 시작일", example = "2024-11-16")
@NotNull(message = "시작일은 비어있을 수 없습니다.")
LocalDate startDate,
@Schema(description = "여행 날짜 정보")
@NotNull(message = "여행 날짜 정보는 비어있을 수 없습니다.")
List<PlanDayCreateRequest> days
) {

public TravelPlan toTravelPlan() {
return new TravelPlan(title, startDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package woowacourse.touroot.travelplan.dto;

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

public record TravelPlanCreateResponse(
@Schema(description = "생성된 여행 계획 id")
Long id
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package woowacourse.touroot.travelplan.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.travelplan.domain.TravelPlanDay;

public interface TravelPlanDayRepository extends JpaRepository<TravelPlanDay, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package woowacourse.touroot.travelplan.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.travelplan.domain.TravelPlanPlace;

public interface TravelPlanPlaceRepository extends JpaRepository<TravelPlanPlace, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package woowacourse.touroot.travelplan.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import woowacourse.touroot.travelplan.domain.TravelPlan;

public interface TravelPlanRepository extends JpaRepository<TravelPlan, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package woowacourse.touroot.travelplan.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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.repository.TravelPlanDayRepository;
import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository;
import woowacourse.touroot.travelplan.repository.TravelPlanRepository;

import java.util.List;

@RequiredArgsConstructor
@Service
public class TravelPlanService {

private final TravelPlanRepository travelPlanRepository;
private final TravelPlanDayRepository travelPlanDayRepository;
private final TravelPlanPlaceRepository travelPlanPlaceRepository;
private final PlaceRepository placeRepository;

@Transactional
public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request) {
TravelPlan travelPlan = request.toTravelPlan();
travelPlan.validateStartDate();

TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan);
createPlanDay(request, savedTravelPlan);

return new TravelPlanCreateResponse(savedTravelPlan.getId());
}

private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTravelPlan) {
for (PlanDayCreateRequest dayRequest : request.days()) {
TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(savedTravelPlan));
createPlanPlace(dayRequest.places(), travelPlanDay);
}
}

private void createPlanPlace(List<PlanPlaceCreateRequest> request, TravelPlanDay travelPlanDay) {
for (PlanPlaceCreateRequest planRequest : request) {
Place place = getPlace(planRequest);
travelPlanPlaceRepository.save(planRequest.toPlanPlace(travelPlanDay, place));
}
}

private Place getPlace(PlanPlaceCreateRequest planRequest) {
return placeRepository.findByNameAndLatitudeAndLongitude(
planRequest.placeName(),
planRequest.location().lat(),
planRequest.location().lng()
).orElseGet(() -> placeRepository.save(planRequest.toPlace()));
}
}
Loading
Loading