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

feat: 예약 조기종료 기능 구현 #969

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
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
Expand Up @@ -4,7 +4,20 @@
import com.woowacourse.zzimkkong.domain.LoginEmail;
import com.woowacourse.zzimkkong.domain.ReservationType;
import com.woowacourse.zzimkkong.dto.member.LoginUserEmail;
import com.woowacourse.zzimkkong.dto.reservation.*;
import com.woowacourse.zzimkkong.dto.reservation.ReservationAuthenticationDto;
import com.woowacourse.zzimkkong.dto.reservation.ReservationCreateDto;
import com.woowacourse.zzimkkong.dto.reservation.ReservationCreateResponse;
import com.woowacourse.zzimkkong.dto.reservation.ReservationCreateUpdateWithPasswordRequest;
import com.woowacourse.zzimkkong.dto.reservation.ReservationEarlyStopDto;
import com.woowacourse.zzimkkong.dto.reservation.ReservationEarlyStopRequest;
import com.woowacourse.zzimkkong.dto.reservation.ReservationFindAllDto;
import com.woowacourse.zzimkkong.dto.reservation.ReservationFindAllResponse;
import com.woowacourse.zzimkkong.dto.reservation.ReservationFindDto;
import com.woowacourse.zzimkkong.dto.reservation.ReservationFindResponse;
import com.woowacourse.zzimkkong.dto.reservation.ReservationInfiniteScrollResponse;
import com.woowacourse.zzimkkong.dto.reservation.ReservationPasswordAuthenticationRequest;
import com.woowacourse.zzimkkong.dto.reservation.ReservationResponse;
import com.woowacourse.zzimkkong.dto.reservation.ReservationUpdateDto;
import com.woowacourse.zzimkkong.dto.slack.SlackResponse;
import com.woowacourse.zzimkkong.infrastructure.datetime.TimeZoneUtils;
import com.woowacourse.zzimkkong.service.ReservationService;
Expand All @@ -14,7 +27,16 @@
import org.springframework.data.web.PageableDefault;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.net.URI;
Expand All @@ -23,7 +45,6 @@

import static com.woowacourse.zzimkkong.dto.ValidatorMessage.DATETIME_FORMAT;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.DATE_FORMAT;
import static com.woowacourse.zzimkkong.infrastructure.datetime.TimeZoneUtils.UTC;

@LogMethodExecutionTime(group = "controller")
@RestController
Expand Down Expand Up @@ -153,6 +174,25 @@ public ResponseEntity<Void> update(
return ResponseEntity.ok().build();
}

@PatchMapping("/maps/{mapId}/spaces/{spaceId}/reservations/{reservationId}")
public ResponseEntity<Void> earlyStop(
@PathVariable final Long mapId,
@PathVariable final Long spaceId,
@PathVariable final Long reservationId,
@RequestBody @Valid final ReservationEarlyStopRequest reservationEarlyStopRequest,
@LoginEmail(isOptional = true) final LoginUserEmail loginUserEmail) {
ReservationEarlyStopDto reservationUpdateDto = ReservationEarlyStopDto.of(
mapId,
spaceId,
reservationId,
reservationEarlyStopRequest,
loginUserEmail,
ReservationType.Constants.GUEST);
SlackResponse slackResponse = reservationService.earlyStop(reservationUpdateDto);
slackService.sendEarlyStopMessage(slackResponse);
return ResponseEntity.ok().build();
}

@DeleteMapping("/maps/{mapId}/spaces/{spaceId}/reservations/{reservationId}")
public ResponseEntity<Void> delete(
@PathVariable final Long mapId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
package com.woowacourse.zzimkkong.domain;

import com.woowacourse.zzimkkong.exception.reservation.InvalidMinimumDurationTimeInEarlyStopException;
import com.woowacourse.zzimkkong.exception.reservation.NotCurrentReservationException;
import com.woowacourse.zzimkkong.exception.setting.NoMatchingSettingException;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.logstash.logback.encoder.org.apache.commons.lang3.StringUtils;

import javax.persistence.*;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ForeignKey;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

@Getter
@Builder
Expand Down Expand Up @@ -82,6 +96,10 @@ public void update(final Reservation updateReservation) {
this.space = updateReservation.space;
}

public void updateReservationTime(final ReservationTime updateReservationTime) {
this.reservationTime = updateReservationTime;
}

public LocalDateTime getStartTime() {
return reservationTime.getStartTime();
}
Expand Down Expand Up @@ -142,4 +160,36 @@ public String getUserName() {
}
return this.userName;
}

public TimeUnit getReservationTimeUnit() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public TimeUnit getReservationTimeUnit() {
private TimeUnit getReservationTimeUnit() {

Reservation 안에서만 쓰이는 것으로 보여요

Setting setting = space.getSpaceSettings().getSettings()
.stream()
.filter(it -> it.supports(getTimeSlot(), getDayOfWeek()))
.findFirst()
.orElseThrow(NoMatchingSettingException::new);

return setting.getReservationTimeUnit();
}

public void doEarlyClose(final LocalDateTime now, final Map map) {
validateEarlyCloseable(now);

int endTimeFloor = this.getReservationTimeUnit().floor(now);

this.reservationTime = ReservationTime.of(
reservationTime.getStartTime(),
now.withMinute(endTimeFloor),
map.getServiceZone(),
false);
}

private void validateEarlyCloseable(final LocalDateTime now) {
if (!this.isInUse(now)) {
throw new NotCurrentReservationException();
}

if (ChronoUnit.MINUTES.between(this.getStartTime(), now) < this.getReservationTimeUnit().getMinutes()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this.getReservationTimeUnit()가 총 2번 호출되는 것으로 보입니다. 1번만 호출하는 게 로직이 더 효율적일 것 같습니다.

throw new InvalidMinimumDurationTimeInEarlyStopException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,3 @@ public String toString() {
return startTime + " ~ " + endTimeAsString;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -62,6 +63,10 @@ public boolean isShorterThan(final TimeUnit that) {
return this.minutes < that.minutes;
}

public int floor(final LocalDateTime time) {
return time.getMinute() / this.minutes * this.minutes;
}
Comment on lines +66 to +68
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍🏿👍🏿👍🏿👍🏿


@Override
public String toString() {
int hours = this.minutes / 60;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.woowacourse.zzimkkong.dto.reservation;

import com.woowacourse.zzimkkong.domain.ReservationType;
import com.woowacourse.zzimkkong.dto.member.LoginUserEmail;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ReservationEarlyStopDto {

private Long mapId;
private Long spaceId;
private Long reservationId;
private String email;
private String password;
Comment on lines +15 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

email 필드는 조기종료 기능에서는 따로 받을 필요 없을 것 같아요~ 이미 Controller 에서 @LoginEmail final LoginUserEmail loginUserEmail 를 받고 계시기 때문에 ReservationService에 작성해주신 것 처럼 해당 객체를 사용하면 됩니다.

좀 헷갈릴 수 있어서 추가 설명 드리면, 다른 DTO 들에서 보이는 email 필드는 공간 관리자 MANAGER (e.g. 잠실, 선릉 맵 매니저 = 포비, 제이슨 등) 가 다른 회원들의 예약을 대신 매니징해줄 때 이메일이 필요한 경우 (회원 유저)가 있어서 따로 받기 위해서 별도로 존재하는 필드라고 생각해주시면 됩니다.
관련 로직이 궁금하시면 ManagerLoginGuestReservationStrategy.buildReservation 부분 참고 해주시면 됩니다. 번외로 password 는 비회원 유저용 필드입니다!

private LoginUserEmail loginUserEmail;
private ReservationType reservationType;

private ReservationEarlyStopDto(
final Long mapId,
final Long spaceId,
final Long reservationId,
final ReservationEarlyStopRequest request,
final LoginUserEmail loginUserEmail,
final String apiType) {
this.reservationId = reservationId;
this.spaceId = spaceId;
this.mapId = mapId;
this.password = request.getPassword();
this.email = request.getEmail();
this.loginUserEmail = loginUserEmail;
this.reservationType = ReservationType.of(apiType, getReservationEmail(apiType));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
this.reservationType = ReservationType.of(apiType, getReservationEmail(apiType));
this.reservationType = ReservationType.of(ReservationType.Constants.GUEST, this.loginUserEmail);

매니저 Controller 쪽에도 동일한 API를 뚫을 게 아니라면 이렇게 고정되어도 괜찮겠네요. DTO 생성 시 불필요하게 주입받는 인자도 줄이고. 저희 기존 코드 틀을 지켜주시려고 신경을 많이 써주시다 보니 불필요하게 레퍼런스 되는 부분이 있는 것 같아요. 그래서 코드 이해를 도와드리고 사고를 더 확장? 시켜드리기 위해 드리는 제안입니다 ㅎㅎ. 확장하기 용이한 지금 이 형태가 좋다면 꼭 반영 안하셔도 됩니다! 회의 때 여러분이 나눈 의견에 더 적합한 방향으로 구현 해주세요.

}

public static ReservationEarlyStopDto of(
final Long mapId,
final Long spaceId,
final Long reservationId,
final ReservationEarlyStopRequest request,
final LoginUserEmail loginUserEmail,
final String apiType) {
return new ReservationEarlyStopDto(
mapId,
spaceId,
reservationId,
request,
loginUserEmail,
apiType);
}

public static ReservationEarlyStopDto of(
final Long mapId,
final Long spaceId,
final Long reservationId,
final ReservationEarlyStopRequest request,
final String apiType) {
return new ReservationEarlyStopDto(
mapId,
spaceId,
reservationId,
request,
new LoginUserEmail(),
apiType);
}

private LoginUserEmail getReservationEmail(String apiType) {
if (ReservationType.Constants.GUEST.equals(apiType)) {
return this.loginUserEmail;
}
return LoginUserEmail.from(this.email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.woowacourse.zzimkkong.dto.reservation;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import static com.woowacourse.zzimkkong.dto.ValidatorMessage.DESCRIPTION_MESSAGE;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.EMPTY_MESSAGE;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.NAME_MESSAGE;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.NAMING_FORMAT;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.RESERVATION_PW_FORMAT;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.RESERVATION_PW_MESSAGE;

@Getter
@NoArgsConstructor
public class ReservationEarlyStopRequest {
@Pattern(regexp = RESERVATION_PW_FORMAT, message = RESERVATION_PW_MESSAGE)
private String password;

@Pattern(regexp = NAMING_FORMAT, message = NAME_MESSAGE)
protected String name;

@NotBlank(message = EMPTY_MESSAGE)
@Size(max = 100, message = DESCRIPTION_MESSAGE)
protected String description;

public String getPassword() {
return null;
}

public String getEmail() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.woowacourse.zzimkkong.dto.reservation;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import static com.woowacourse.zzimkkong.dto.ValidatorMessage.DESCRIPTION_MESSAGE;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.EMPTY_MESSAGE;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.NAME_MESSAGE;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.NAMING_FORMAT;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.RESERVATION_PW_FORMAT;
import static com.woowacourse.zzimkkong.dto.ValidatorMessage.RESERVATION_PW_MESSAGE;

@Getter
@NoArgsConstructor
public class ReservationManagerEarlyStopRequest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

사용하는 곳이 없네요?

@Pattern(regexp = RESERVATION_PW_FORMAT, message = RESERVATION_PW_MESSAGE)
private String password;

@Pattern(regexp = NAMING_FORMAT, message = NAME_MESSAGE)
protected String name;

@NotBlank(message = EMPTY_MESSAGE)
@Size(max = 100, message = DESCRIPTION_MESSAGE)
protected String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ public static Attachments updateMessageOf(final SlackResponse slackResponse,
return Attachments.from(attachment);
}

public static Attachments earlyStopMessageOf(final SlackResponse slackResponse,
final String titleLink) {
Attachment attachment = Attachment.of(
"🙏 예약 종료 알림 🙏",
COLOR,
"🙏 예약이 종료되었습니다.",
TITLE_LINK_MESSAGE,
titleLink + GUEST_URI + slackResponse.getSharingMapId(),
slackResponse);
return Attachments.from(attachment);
}

public static Attachments deleteMessageOf(final SlackResponse slackResponse,
final String titleLink) {
Attachment attachment = Attachment.of(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.woowacourse.zzimkkong.exception.reservation;

import com.woowacourse.zzimkkong.exception.ZzimkkongException;
import org.springframework.http.HttpStatus;

public class InvalidMinimumDurationTimeInEarlyStopException extends ZzimkkongException {

private static final String MESSAGE = "조기 종료는 최소 5분 이후부터 가능합니다.";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
private static final String MESSAGE = "조기 종료는 최소 5분 이후부터 가능합니다.";
private static final String MESSAGE = "조기 종료는 최소 X분 이후부터 가능합니다.";

분이 가변적이어야 할 것 같아요~ Exception 생성자에 TimeUnit 관련 인자를 받아야겠네요. 참고할 만한 Exception 많으니 한 번 둘러보셔서 참고하시면 될 것 같습니다!


public InvalidMinimumDurationTimeInEarlyStopException() {
super(MESSAGE, HttpStatus.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.woowacourse.zzimkkong.exception.reservation;

import com.woowacourse.zzimkkong.exception.ZzimkkongException;
import org.springframework.http.HttpStatus;

public class NotCurrentReservationException extends ZzimkkongException {
private static final String MESSAGE = "사용중인 예약에 대해서만 조기종료가 가능합니다.";

public NotCurrentReservationException() {
super(MESSAGE, HttpStatus.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.woowacourse.zzimkkong.exception.setting;

import com.woowacourse.zzimkkong.exception.ZzimkkongException;
import org.springframework.http.HttpStatus;

public class NoMatchingSettingException extends ZzimkkongException {
private static final String MESSAGE = "일치하는 설정이 존재하지 않습니다.";

public NoMatchingSettingException() {
super(MESSAGE, HttpStatus.NOT_FOUND);
Copy link
Collaborator

Choose a reason for hiding this comment

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

500과 같은 서버 에러가 적합해 보여요. setting 은 무조건 매칭되는 한개가 있어야합니다. 만약 없다면 모종의 이유로 발생한 데이터 이슈입니다.

}
}
Loading