장바구니 api - Create 추가 #1 (Feat. SpringBoot) 에서 설명한 Table 구조에 따른 장바구니 추가 api 코드에 대해 설명하겠다. v1와 v2 기간을 거치면서 장바구니 api를 고도화했다. 간단한 api일줄 알았으나, 직접 개발을 하면서 여러 에러를 겪고 해결하며 성장할 수 있었다. 🌱
로직 설명
단기투숙
- startDate = 2023-11-29
- endDate = 2023-11-30
- roomProduct |2023-11-29| stock : 2
- 장바구니 = 11월 29일만 담긴다.
- 만약, 11월 29일의 재고가 이미 cart에 2번 담겨있다면,
“상품의 재고 부족으로 장바구니 담기가 불가합니다.
”이 발생한다.
장기투숙
- startDate = 2023-11-29
- endDate = 2023-12-01
- room1번 =⇒ 장바구니
- roomProduct |2023-11-29| stock : 2 | room1
- roomProduct |2023-11-30| stock : 4 | room1
- roomProduct |2023-12-01| stock : 6 | room1
- 장바구니 = 11월 29일~ 11월 30일 까지 담긴다. (12월 1일은 담기지 않는다.)
- 만약, 11월 29일 ~ 11월 30일까지 room의 재고가 0인 순간이 있다면 장바구니에 담길 수 없다.
(-> 애초에 이런 경우라면 숙소 상세 조회시, 안나옵니다!)
- 11월29일~11월30일 의 최소재고가 2개이므로, 11월29일~11월30일 room1번은 장바구니에 2번만 담길 수 있다.
⇒ 만약, 2번 이상 담으려고 하면
“
상품의 재고 부족으로 장바구니 담기가 불가합니다.
” 발생
예외처리
- startDate 가 endDate보다 후순위 일 경우 →
“시작일/종료일을 다시 확인해주세요.”
- 만약, roomProduct가 11월29일~11월 30일까지만 있는데, 12월1일~12월3일을 요청시 →
Room Product가 존재하지 않습니다.
개발 에러 상황
- 단기투숙 ↔ 장기투숙을 구별하지 않았다.
- 11월 01일 ~ 11월 03일 투숙시, 11월 1일과 2일만 roomCart에 담겨야 했다.
- 단기투숙 ↔ 장기투숙을 구별한 뒤, 로직을 전체 수정해야했는데 다 수정하지 못했다.
⇒ Repository ERROR
V1 service
@Service
@Transactional
@RequiredArgsConstructor
public class RoomCartService {
private final RoomRepository roomRepository;
private final CartRepository cartRepository;
private final RoomCartRepository roomCartRepository;
public RoomCartResponseDTO postRoomCart(Long member_id, Long room_id) {
Room room = roomRepository.findById(room_id).get();
Cart cart = cartRepository.findByMemberId(member_id).get();
if (room.getStock() > 0) {
room.updateRoomStock(room.getStock() - 1);
RoomCart roomCart = roomCartRepository.save(new RoomCart(cart, room));
cart.postRoomCarts(roomCart);
return new RoomCartResponseDTO(cart);
} else {
throw new OutOfStockException();
}
}
}
V2 service
- RoomProduct table 생성
문제발생…⚠️💣
- 시작날짜, 종료날짜가 포함되어 객실의 stock을 체크해야하는게 아닌가 ?
- 결과적으로, RoomCart Table에 startDate, endDate가 포함되어야하는게 아닌가?
EXAMPLE
- 11월 3일 ~ 11월 4일 숙소#1 방#1 : 4개
- 11월 3일 ~ 11월 4일 숙소#1 방#2 : 1개
⇒ 숙소#1 방#1은 장바구니에 4번 담을 수 있다.
RequestDto(roomId, startDate, endDate)
RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 3개
RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 2개
RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 1개
RequestDto(1,11월3일,11월4일) → 장바구니 담김 → 숙소#1 방#1 : 0개
RequestDto(1,11월3일,11월4일) → 장바구니 담김 → OutOfStockException
결과적으로, 계속 Room의 Stock Update가 필요한데, 이게 DB에 저장이 되어야하니까 객실에 Stock 칼럼 외에 CntStock이 또 추가되어야하는게 아닌가?
- 임시방편으로 생각해본 해결방안..
프론트측에서 Stock을 알고있으니 OutOfStockException을 맡고, 백에서는 검증하지 않는다.Ajax라는걸쓰면 순간적으로 Stock의 개수를 - 해서 검증할 수 있다고 하는데 이게 프론트 영역에서 검증하는 방식인것같습니다.
아니면, 장바구니 버튼 클릭으로 넘어가지 말고,장바구니 버튼 클릭 → 몇개 담으실건가요? 팝업창 (개수 확인) → 그 개수보다 만약 객실 stock이 적다면 error⇒ 이 루틴으로 간다면 프론트한테 데이터 넘겨받고 일이 2번이 된다…
Room Entity에 CntStock 컬럼을 추가하여 Update(담길때마다 -1)하여 관리한다.
- 시작날짜, 종료날짜가 포함되어 객실의 stock을 체크해야하는게 아닌가 ?
변경사항
- Cart가 존재하지 않을 경우, Cart 생성
- checkContinualDate : 해당 기간동안 roomProduct가 모두 List에 담겼는지 확인 메서드 구현
- findMinStockRoomProduct : 가장 적은 재고를 가진 RoomProduct를 찾는 메서드 구현
- roomProduct의 stock에서 가장 적은 재고를 가진 roomProduct가 roomCart에 담긴 횟수를 빼서 0보다 크면 roomCart에 저장
- roomCartRepository에 이를 저장하고 cart의 List에 추가
@Transactional
public RoomCartResponse postRoomCart(Long member_id, Long room_id,
RoomCartRequest roomCartRequest) {
Optional<Cart> optionalCart = cartRepository.findByMemberId(member_id);
Cart cart = optionalCart.orElseGet(() -> {
Member member = memberRepository.findById(member_id).orElseThrow(
MemberNotFoundException::new);
return cartRepository.save(new Cart(member));
});
List<RoomProduct> roomProductList = roomProductRepository.findByRoomIdAndStartDateAndEndDate(
room_id,
roomCartRequest.getStartDate(), roomCartRequest.getEndDate());
checkContinualDate(roomProductList, roomCartRequest);
RoomProduct roomProductMinStock = findMinStockRoomProduct(roomProductList);
for (RoomProduct roomProduct : roomProductList) {
List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(
roomProductMinStock.getId());
if (roomProduct.getStock() - roomCartList.size() > 0) {
RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct).build();
roomCartRepository.save(roomCart);
cart.postRoomCarts(roomCart);
} else {
throw new OutOfStockException();
}
}
return new RoomCartResponse(cart);
}
private void checkContinualDate(List<RoomProduct> roomProductList,
RoomCartRequest roomCartRequest) {
LocalDate startDate = roomCartRequest.getStartDate();
LocalDate endDate = roomCartRequest.getEndDate();
long betweenDays = ChronoUnit.DAYS.between(startDate, endDate);
if (roomProductList.size() != betweenDays) {
throw new RoomProductNotFoundException();
}
}
private RoomProduct findMinStockRoomProduct(List<RoomProduct> roomProductList) {
int minStock = Integer.MAX_VALUE;
for (RoomProduct roomProduct : roomProductList) {
minStock = Math.min(roomProduct.getStock(), minStock);
}
RoomProduct minStockRoomProduct = roomProductRepository.findByStock(minStock).get();
return minStockRoomProduct;
}
V2 service #2
변경이유
- 단기투숙과 장기투숙 로직을 구별하고 수정했습니다.
- 단기투숙시, startDate의 roomProduct만 넘기고
- 장기투숙시, startDate ~ endDate-1만 넘겨야 하므로 수정했습니다.
- RoomProductNotFound Exception Handler를 추가했습니다.
@Transactional
public RoomCartResponse postRoomCart(Long member_id, Long room_id,
RoomCartRequest roomCartRequest) {
checkStartDateEndDate(roomCartRequest);
Optional<Cart> optionalCart = cartRepository.findByMemberId(member_id);
Cart cart = optionalCart.orElseGet(() -> {
Member member = memberRepository.findById(member_id).orElseThrow(
MemberNotFoundException::new);
return cartRepository.save(new Cart(member));
});
List<RoomProduct> roomProductList;
if(roomCartRequest.getStartDate().equals(roomCartRequest.getEndDate().minusDays(1))){
roomProductList = roomProductRepository.findByRoomIdAndStartDate(
room_id, roomCartRequest.getStartDate());
} else {
roomProductList = roomProductRepository.findByRoomIdAndStartDateAndEndDate(
room_id,
roomCartRequest.getStartDate(), roomCartRequest.getEndDate().minusDays(1));
}
checkContinualDate(roomProductList, roomCartRequest);
RoomProduct roomProductMinStock = findMinStockRoomProduct(roomProductList);
for (RoomProduct roomProduct : roomProductList) {
List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(
roomProductMinStock.getId());
if (roomProduct.getStock() - roomCartList.size() > 0) {
RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct).build();
roomCartRepository.save(roomCart);
cart.postRoomCarts(roomCart);
} else {
throw new OutOfStockException();
}
}
return new RoomCartResponse(cart);
}
private void checkContinualDate(List<RoomProduct> roomProductList,
RoomCartRequest roomCartRequest) {
LocalDate startDate = roomCartRequest.getStartDate();
LocalDate endDate = roomCartRequest.getEndDate();
long betweenDays = ChronoUnit.DAYS.between(startDate, endDate);
if (roomProductList.size() != betweenDays) {
throw new RoomProductNotFoundException();
}
}
private RoomProduct findMinStockRoomProduct(List<RoomProduct> roomProductList) {
int minStock = Integer.MAX_VALUE;
for (RoomProduct roomProduct : roomProductList) {
minStock = Math.min(roomProduct.getStock(), minStock);
}
RoomProduct minStockRoomProduct = roomProductRepository.findByStock(minStock).get();
return minStockRoomProduct;
}
private void checkStartDateEndDate(RoomCartRequest roomCartRequest) {
LocalDate startDate = roomCartRequest.getStartDate();
LocalDate endDate = roomCartRequest.getEndDate();
if(startDate.isAfter(endDate)){
throw new WrongDateException();
}
}
V2 service #3
변경이유
ERROR 발생
⚠️ UnsupportedOperationException
⚠️ query did not return a unique result
수정사항
- 로직 변경으로 personnel 추가
🔗 참고 url : https://yusang.tistory.com/43
- 단기투숙 ↔ 장기투숙 로직 완전 다르게 진행
@Transactional
public RoomCartResponse postRoomCart(Long member_id, Long room_id,
RoomCartRequest roomCartRequest) {
checkStartDateEndDate(roomCartRequest);
Optional<Cart> optionalCart = cartRepository.findByMemberId(member_id);
Cart cart = optionalCart.orElseGet(() -> {
Member member = memberRepository.findById(member_id).orElseThrow(
MemberNotFoundException::new);
return cartRepository.save(new Cart(member));
});
List<RoomProduct> roomProductList = new ArrayList<>();
if(roomCartRequest.getStartDate().equals(roomCartRequest.getEndDate().minusDays(1))){
RoomProduct roomProduct = roomProductRepository.findByRoomIdAndStartDate(
room_id, roomCartRequest.getStartDate()).orElseThrow(RoomProductNotFoundException::new);
List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(roomProduct.getId());
if(roomProduct.getStock() - roomCartList.size() > 0) {
RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct)
.personnel(roomCartRequest.getPersonnel()).build();
roomCartRepository.save(roomCart);
cart.postRoomCarts(roomCart);
return new RoomCartResponse(cart);
} else {
throw new OutOfStockException();
}
} else {
roomProductList = roomProductRepository.findByRoomIdAndStartDateAndEndDate(
room_id,
roomCartRequest.getStartDate(), roomCartRequest.getEndDate().minusDays(1));
checkContinualDate(roomProductList, roomCartRequest);
RoomProduct roomProductMinStock = findMinStockRoomProduct(roomProductList);
for (RoomProduct roomProduct : roomProductList) {
List<RoomCart> roomCartList = roomCartRepository.findByRoomProductId(
roomProductMinStock.getId());
if (roomProduct.getStock() - roomCartList.size() > 0) {
RoomCart roomCart = RoomCart.builder().cart(cart).roomProduct(roomProduct).build();
roomCartRepository.save(roomCart);
cart.postRoomCarts(roomCart);
} else {
throw new OutOfStockException();
}
}
return new RoomCartResponse(cart);
}
}
private void checkContinualDate(List<RoomProduct> roomProductList,
RoomCartRequest roomCartRequest) {
LocalDate startDate = roomCartRequest.getStartDate();
LocalDate endDate = roomCartRequest.getEndDate();
long betweenDays = ChronoUnit.DAYS.between(startDate, endDate);
if (roomProductList.size() != betweenDays) {
throw new RoomProductNotFoundException();
}
}
<-- 변경 사항 -->
private RoomProduct findMinStockRoomProduct(List<RoomProduct> roomProductList) {
roomProductList.sort(new Comparator<RoomProduct>() {
@Override
public int compare(RoomProduct o1, RoomProduct o2) {
if(o1.getStock() > o2.getStock()){
return 1;
} else if (o1.getStock() < o2.getStock()) {
return -1;
}else {
return 0;
}
}
});
return roomProductList.get(0);
}
<------------->
private void checkStartDateEndDate(RoomCartRequest roomCartRequest) {
LocalDate startDate = roomCartRequest.getStartDate();
LocalDate endDate = roomCartRequest.getEndDate();
if(startDate.isAfter(endDate)){
throw new WrongDateException();
}
}
배운점
- yaml파일의 profile active : dev 로 되어있으면 RDS랑 연결된다.
- Putty로 ssh 접속
- cd app
- tail -f nohup.out : error 확인 가능
⇒ http://43.202.50.38:8080/v2/accommodations/356?startDate=2023-11-29&endDate=2023-11-30&personnel=1
localhost로 수정 ⇒ http://localhost:8080/v2/accommodations/356?startDate=2023-11-29&endDate=2023-11-30&personnel=1 으로 보면, 이제 log들을 IntelliJ에서 볼 수 있다.
- 생각보다 예외처리가 다양하고 꼼꼼히 해야한다.
사용자 입장에서 이 api를 통해 서비스를 사용했을 때, 매끄럽게 운영되어야 한다. 그러한 점에서 개발하기 전에 로직을 꼼꼼히 작성해보고, 예외처리를 구체적으로 작성해둬야함을 깨닫게 됐다.
Uploaded by N2T