pricelees ed383c3092 [#11] Payment 도메인 코드 코틀린 마이그레이션 (#12)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR
**PR과 관련된 이슈 번호**
- #11

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
payment 패키지 내 코드, 테스트를 코틀린으로 전환했고 일부 로직은 개선하였음. 전체적으로 구조를 개선하려고 했으나, 얽혀있는 예약 관련 로직이 많아 전체 코드의 코틀린 전환이 끝난 이후 개선할 예정

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
1. \@DataJpaTest를 이용하는 Repository 테스트를 추가
2. Service는 mocking 방식으로 수정하였고, 테스트가 불필요하다고 여겨지는 단순 로직(변환 또는 Repository만 사용하는 경우)은 제외하였음. (8577b68496)
- 전체 로직이 테스트되어있는 기존의 테스트는 유지하였고, 전체 코틀린 전환이 마무리 된 후 제거 예정

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #12
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-07-16 09:19:28 +00:00

272 lines
13 KiB
Java

package roomescape.reservation.controller;
import java.time.LocalDate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.headers.Header;
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.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import roomescape.auth.web.support.Admin;
import roomescape.auth.web.support.LoginRequired;
import roomescape.auth.web.support.MemberId;
import roomescape.common.dto.response.RoomescapeApiResponse;
import roomescape.common.dto.response.RoomescapeErrorResponse;
import roomescape.common.exception.RoomescapeException;
import roomescape.payment.infrastructure.client.TossPaymentClient;
import roomescape.payment.web.PaymentApprove;
import roomescape.payment.web.PaymentCancel;
import roomescape.reservation.dto.request.AdminReservationRequest;
import roomescape.reservation.dto.request.ReservationRequest;
import roomescape.reservation.dto.request.WaitingRequest;
import roomescape.reservation.dto.response.MyReservationsResponse;
import roomescape.reservation.dto.response.ReservationResponse;
import roomescape.reservation.dto.response.ReservationsResponse;
import roomescape.reservation.service.ReservationService;
import roomescape.reservation.service.ReservationWithPaymentService;
@RestController
@Tag(name = "3. 예약 API", description = "예약 및 대기 정보를 추가 / 조회 / 삭제할 때 사용합니다.")
public class ReservationController {
private final ReservationWithPaymentService reservationWithPaymentService;
private final ReservationService reservationService;
private final TossPaymentClient paymentClient;
public ReservationController(ReservationWithPaymentService reservationWithPaymentService,
ReservationService reservationService, TossPaymentClient paymentClient) {
this.reservationWithPaymentService = reservationWithPaymentService;
this.reservationService = reservationService;
this.paymentClient = paymentClient;
}
@Admin
@GetMapping("/reservations")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "모든 예약 정보 조회", tags = "관리자 로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
})
public RoomescapeApiResponse<ReservationsResponse> getAllReservations() {
return RoomescapeApiResponse.success(reservationService.findAllReservations());
}
@LoginRequired
@GetMapping("/reservations-mine")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "자신의 예약 및 대기 조회", tags = "로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
})
public RoomescapeApiResponse<MyReservationsResponse> getMemberReservations(
@MemberId @Parameter(hidden = true) Long memberId) {
return RoomescapeApiResponse.success(reservationService.findMemberReservations(memberId));
}
@Admin
@GetMapping("/reservations/search")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = "관리자 로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true),
@ApiResponse(responseCode = "400", description = "날짜 범위를 지정할 때, 종료 날짜는 시작 날짜 이전일 수 없습니다.",
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
})
public RoomescapeApiResponse<ReservationsResponse> getReservationBySearching(
@RequestParam(required = false) @Parameter(description = "테마 ID") Long themeId,
@RequestParam(required = false) @Parameter(description = "회원 ID") Long memberId,
@RequestParam(required = false) @Parameter(description = "yyyy-MM-dd 형식으로 입력해주세요", example = "2024-06-10") LocalDate dateFrom,
@RequestParam(required = false) @Parameter(description = "yyyy-MM-dd 형식으로 입력해주세요", example = "2024-06-10") LocalDate dateTo
) {
return RoomescapeApiResponse.success(
reservationService.findFilteredReservations(themeId, memberId, dateFrom, dateTo));
}
@Admin
@DeleteMapping("/reservations/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "관리자의 예약 취소", tags = "관리자 로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "성공"),
@ApiResponse(responseCode = "404", description = "예약 또는 결제 정보를 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))),
})
public RoomescapeApiResponse<Void> removeReservation(
@MemberId @Parameter(hidden = true) Long memberId,
@NotNull(message = "reservationId는 null일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
) {
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
reservationService.removeReservationById(reservationId, memberId);
return RoomescapeApiResponse.success();
}
PaymentCancel.Request paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
reservationId, memberId);
PaymentCancel.Response paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest);
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
paymentCancelResponse.canceledAt);
return RoomescapeApiResponse.success();
}
@LoginRequired
@PostMapping("/reservations")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "예약 추가", tags = "로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true,
headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1")))
})
public RoomescapeApiResponse<ReservationResponse> saveReservation(
@Valid @RequestBody ReservationRequest reservationRequest,
@MemberId @Parameter(hidden = true) Long memberId,
HttpServletResponse response
) {
PaymentApprove.Request paymentRequest = reservationRequest.getPaymentRequest();
PaymentApprove.Response paymentResponse = paymentClient.confirmPayment(paymentRequest);
try {
ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment(
reservationRequest, paymentResponse, memberId);
return getCreatedReservationResponse(reservationResponse, response);
} catch (RoomescapeException e) {
PaymentCancel.Request cancelRequest = new PaymentCancel.Request(paymentRequest.paymentKey,
paymentRequest.amount, e.getMessage());
PaymentCancel.Response paymentCancelResponse = paymentClient.cancelPayment(cancelRequest);
reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
paymentRequest.paymentKey);
throw e;
}
}
@Admin
@PostMapping("/reservations/admin")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "관리자 예약 추가", tags = "관리자 로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true,
headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1"))),
@ApiResponse(responseCode = "409", description = "예약이 이미 존재합니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
})
public RoomescapeApiResponse<ReservationResponse> saveReservationByAdmin(
@Valid @RequestBody AdminReservationRequest adminReservationRequest,
HttpServletResponse response
) {
ReservationResponse reservationResponse = reservationService.addReservationByAdmin(adminReservationRequest);
return getCreatedReservationResponse(reservationResponse, response);
}
@Admin
@GetMapping("/reservations/waiting")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "모든 예약 대기 조회", tags = "관리자 로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
})
public RoomescapeApiResponse<ReservationsResponse> getAllWaiting() {
return RoomescapeApiResponse.success(reservationService.findAllWaiting());
}
@LoginRequired
@PostMapping("/reservations/waiting")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "예약 대기 신청", tags = "로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true,
headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1")))
})
public RoomescapeApiResponse<ReservationResponse> saveWaiting(
@Valid @RequestBody WaitingRequest waitingRequest,
@MemberId @Parameter(hidden = true) Long memberId,
HttpServletResponse response
) {
ReservationResponse reservationResponse = reservationService.addWaiting(waitingRequest, memberId);
return getCreatedReservationResponse(reservationResponse, response);
}
@LoginRequired
@DeleteMapping("/reservations/waiting/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "예약 대기 취소", tags = "로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "성공"),
@ApiResponse(responseCode = "404", description = "회원의 예약 대기 정보를 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
})
public RoomescapeApiResponse<Void> deleteWaiting(
@MemberId @Parameter(hidden = true) Long memberId,
@NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
) {
reservationService.cancelWaiting(reservationId, memberId);
return RoomescapeApiResponse.success();
}
@Admin
@PostMapping("/reservations/waiting/{id}/approve")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "대기 중인 예약 승인", tags = "관리자 로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "404", description = "예약 대기 정보를 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))),
@ApiResponse(responseCode = "409", description = "확정된 예약이 존재하여 대기 중인 예약을 승인할 수 없습니다.",
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
})
public RoomescapeApiResponse<Void> approveWaiting(
@MemberId @Parameter(hidden = true) Long memberId,
@NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
) {
reservationService.approveWaiting(reservationId, memberId);
return RoomescapeApiResponse.success();
}
@Admin
@PostMapping("/reservations/waiting/{id}/deny")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "대기 중인 예약 거절", tags = "관리자 로그인이 필요한 API")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
@ApiResponse(responseCode = "404", description = "예약 대기 정보를 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
})
public RoomescapeApiResponse<Void> denyWaiting(
@MemberId @Parameter(hidden = true) Long memberId,
@NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
) {
reservationService.denyWaiting(reservationId, memberId);
return RoomescapeApiResponse.success();
}
private RoomescapeApiResponse<ReservationResponse> getCreatedReservationResponse(
ReservationResponse reservationResponse,
HttpServletResponse response
) {
response.setHeader(HttpHeaders.LOCATION, "/reservations/" + reservationResponse.id());
return RoomescapeApiResponse.success(reservationResponse);
}
}