개인적인 사정으로 5일간 참여를 못해서 오늘은 좀 달림
일단 info에서 cart로 넘어오는것까지는 했었는데 버튼에 총 금액 부분 안되서 그거 수정하고
그다음에 결재를 하고 orders 에 저장하는 부분에서 생각보다 복잡해
orderMenu와 orderOption라는 엔티티도 추가되고 리뷰작성도 구현함
그리고 페이지에서 값을 받아오는 DTO들도 여럿 생성
처리 순서대로 코드 작성
menu의 main에서 메뉴를 클릭 하면 그 메뉴에 묶인 옵션값이 있다면 같이 가져와서 뿌리는 것 까지는 저번주에 했고 이 값을 cart에 싣기전에 session에 저장하고 id값을 기반으로 데이터를 가져와 뿌림
info.js
이 부분에서 일단 비동기로 id값들을 controller에 전달하고 controller에서 값을 session에 잘 저장하면 다시 와서 cart로 감
function infosubmit(form) {
const selectedOptions = collectSelectedOptions();
const quantity = document.getElementById('order-num').innerText;
const menuId = form.menuId.value;
// JSON 형태로 서버에 데이터 전송
fetch('/menu/info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
menuId: menuId,
selectedOptions: selectedOptions,
quantity: quantity,
}),
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
// 성공적으로 처리되었을 때의 로직
window.location.href = "/menu/cart";
})
.catch((error) => {
console.error('Error:', error);
// 에러 처리 로직
alert('처리 중 에러가 발생했습니다. 메인 화면으로 이동합니다.');
// 메인 화면으로 리디렉션
window.location.href = "/menu/main";
});
// 기본 폼 제출 방지
event.preventDefault();
}
MenuController.java - info의 post매핑 비동기 처리 부분
위의 info에서 비동기로 와서 session에 리스트로 추가를 하고 json형식으로 메시지 반환
// 메뉴 상세 정보 창
@PostMapping("/info")
public ResponseEntity<?> info(@RequestBody OrderDTO orderDTO, HttpSession session) {
System.out.println(orderDTO);
// 세션에서 주문 리스트를 가져옴. 없으면 새 리스트를 생성.
List<OrderDTO> orders = (List<OrderDTO>) session.getAttribute("orders");
if (orders == null) {
orders = new ArrayList<>();
}
// 현재 주문을 리스트에 추가
orders.add(orderDTO);
// 주문 리스트를 세션에 다시 저장
session.setAttribute("orders", orders);
// 정상적인 처리 응답을 JSON 형태로 반환
Map<String, String> response = new HashMap<>();
response.put("message", "주문이 성공적으로 처리되었습니다.");
return ResponseEntity.ok(response);
}
MenuController.java - cart의 get매핑 session에 실린 id값을 검색해서 데이터 가져가는 부분
// 장바구니 페이지 이동 시 session에 값이 있다면 가지고 감
@GetMapping("/cart")
public String cart(HttpSession session, Model model) {
List<OrderDTO> orders = (List<OrderDTO>) session.getAttribute("orders");
List<OrderDetails> orderDetailsList = new ArrayList<>();
for (OrderDTO order : orders) {
OrderDetails orderDetails = ordersService.getOrderDetails(order);
orderDetailsList.add(orderDetails);
}
model.addAttribute("orderDetailsList", orderDetailsList);
return "user/cart"; // 장바구니 페이지로 이동
}
cart.html - 여기서 직접 선택한 옵션과 가격을 보여주고 추가를 하러 메인으로 가거나 결재를 할 수 있음
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>장바구니</title>
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
<link href="/css/style.css" rel="stylesheet">
<link href="/css/user/cart.css" rel="stylesheet" type="text/css">
<script src="/js/user/cart.js" type="text/javascript" defer></script>
<script src="/js/user/validate/iamport.js" type="text/javascript" defer></script>
</head>
<body>
<div class="innerBox">
<header></header>
<div class="container">
<div class="restaurant-name">더조은 식당</div>
<div th:each="orderDetails : ${orderDetailsList}" class="orders"
th:data-menu-price="${orderDetails.menu.price}"
th:data-option-price="${orderDetails.getOptionsPriceSum()}"
th:data-menu-id="${orderDetails.menu.id}">
<div class="menu">
<div class="text">
<!-- 메뉴 제목 -->
<div class="title" th:text="${orderDetails.menu.name}">짜파게티</div>
<!-- 메뉴 가격 -->
<div class="menu-price" th:text="'가격 : ' + ${orderDetails.menu.price} + '원'">가격 : 5,000원</div>
<!-- 옵션 목록 -->
<div th:each="optionDetail : ${orderDetails.optionDetailsList}" class="option">
<span th:text="${optionDetail.menuOption.content} + ' : ' + ${optionDetail.menuOptionValue.content}
+ ' (' + ${optionDetail.menuOptionValue.price} + '원)'">옵션 : 안맵게 해주세요(-3000원)</span>
</div>
<!-- 총 가격은 서버 사이드에서 계산하거나 클라이언트 사이드에서 자바스크립트로 계산할 수 있습니다 -->
<div class="total-price" th:text="'총 가격: ' + ${orderDetails.totalPrice} + '원'">총 가격: ???원</div>
</div>
</div>
<div class="etc">
<div class="image">
<!-- 이미지 경로는 예시입니다. 실제 경로로 변경해야 합니다. -->
<img th:src="@{/images/sample.png}" alt="음식 이미지" class="img">
</div>
<div class="num">
<!-- 수량 조절은 자바스크립트 함수로 구현할 수 있습니다 -->
<input type="button" class="minus" value="-">
<div class="text" th:text="${orderDetails.quantity}">2</div>
<input type="button" class="plus" value="+">
</div>
</div>
</div>
<div class="menu-plus">
+메뉴추가
</div>
<div class="request">
<label for="contents" class="title">요청사항</label>
<textarea class="contents" id="contents" name="contents" placeholder="내용을 입력하세요" rows="4"></textarea>
<input type="button" class="submit button" onclick="requestPay()" value="주문하기">
</div>
</div>
</div>
</body>
</html>
cart.js - 기존에 단순히 숫자 증감만 했다면 이번에는 증감시에 controller에서 비동기로 session 값 갱신
// 숫자 증감
const minus = document.querySelectorAll('.minus');
const plus = document.querySelectorAll('.plus');
// 수량 변경 시 session값을 업데이트
function updateValidSessionQuantity(menuId, newQuantity){
console.log(menuId);
console.log(newQuantity);
// 서버에 수량 변경 요청 보내기
fetch('/menu/updateValidQuantity',{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
menuId: menuId,
quantity: newQuantity,
}),
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch((error) => {
console.error('수량 변경 실패:', error);
});
}
// 수량 변경 함수
function updateQuantityAndPrice(menuId, textElement, change) {
const ordersDiv = textElement.closest('.orders');
const menuPrice = parseInt(ordersDiv.dataset.menuPrice, 10);
const optionPrice = parseInt(ordersDiv.dataset.optionPrice, 10);
const totalPriceElement = ordersDiv.querySelector('.total-price');
// 현재 수량 조절
let quantity = parseInt(textElement.textContent, 10) + change;
quantity = Math.max(1, Math.min(quantity, 100)); // 수량은 1 이상 100 이하로 제한
textElement.textContent = quantity;
updateValidSessionQuantity(menuId, quantity);
// 새로운 총 가격 계산
let newTotalPrice = (menuPrice + optionPrice) * quantity;
totalPriceElement.textContent = '총 가격: ' + newTotalPrice + '원';
// 새로운 총 가격 계산 후 전체 주문 가격 업데이트
updateTotalOrderPrice();
}
// 총 가격 실시간 계산 함수
function updateTotalOrderPrice() {
const orderDivs = document.querySelectorAll('.orders');
let totalOrderPrice = 0;
orderDivs.forEach(div => {
const quantity = parseInt(div.querySelector('.num .text').textContent, 10);
const menuPrice = parseInt(div.dataset.menuPrice, 10);
const optionPrice = parseInt(div.dataset.optionPrice, 10);
totalOrderPrice += (menuPrice + optionPrice) * quantity;
});
// 주문하기 버튼의 값을 업데이트
const submitButton = document.querySelector('.submit.button');
submitButton.value = `${totalOrderPrice}원 주문하기`;
}
// 'minus'와 'plus' 버튼에 이벤트 리스너 추가
minus.forEach(button => {
button.addEventListener('click', () => {
const text = button.closest('.num').querySelector('.text');
const menuId = button.closest('.orders').dataset.menuId;
updateQuantityAndPrice(menuId, text, -1); // 수량 감소
});
});
plus.forEach(button => {
button.addEventListener('click', () => {
const text = button.closest('.num').querySelector('.text');
const menuId = button.closest('.orders').dataset.menuId;
updateQuantityAndPrice(menuId, text, 1); // 수량 증가
});
});
document.addEventListener('DOMContentLoaded', () => {
updateTotalOrderPrice(); // 페이지 로드 시 전체 주문 가격을 계산하고 버튼에 반영
});
// 메뉴 추가 버튼 클릭 시 메인으로
const menuPlus = document.querySelector(".menu-plus");
menuPlus.addEventListener('click', () =>{
location.href = '/menu/main';
});
MainController.java 비동기로 수량 증감시 갱신하는 부분
// cart에서 수량 변경 시 비동기로 업데이트
@PostMapping("/updateValidQuantity")
public ResponseEntity<?> updateMenuQuantity(HttpSession session, @RequestBody OrderDTO orderDTO){
// 세션에서 주문 목록을 가져옴
List<OrderDTO> orders = (List<OrderDTO>) session.getAttribute("orders");
if (orders == null) {
return ResponseEntity.badRequest().body("장바구니가 비어 있습니다.");
}
// 메뉴 ID와 일치하는 주문 찾기 및 수량 업데이트
for (OrderDTO order : orders) {
System.out.println("order.getMenuId() = " + order.getMenuId());
System.out.println("MenuID = " + orderDTO.getMenuId());
System.out.println("Quantity = " + orderDTO.getMenuId());
if (order.getMenuId() == orderDTO.getMenuId()) {
order.setQuantity(orderDTO.getQuantity()); // 수량 업데이트
break; // 메뉴 ID가 일치하는 첫 번째 주문만 업데이트
}
}
// 업데이트된 주문 목록을 세션에 저장
session.setAttribute("orders", orders);
System.out.println(orders);
// 정상적인 처리 응답을 JSON 형태로 반환
Map<String, String> response = new HashMap<>();
response.put("data", "수량이 업데이트되었습니다.");
return ResponseEntity.ok(response);
}
OrderDTO.java - id값만 묶어서 session에 저장할 때 쓰려고 만든 DTO
package kr.ganjuproject.dto;
import lombok.Data;
import java.util.List;
@Data
public class OrderDTO {
private Long menuId;
private List<OptionSelection> selectedOptions;
private int quantity; // 수량 필드 추가
@Data
public static class OptionSelection {
private Long optionId;
private Long valueId;
}
}
iamport.js
결재시 그냥 이름하고 값만 넘기고 결재를 한 다음에 uid값을 받아서 비동기로 저장
// Iamport 결제 라이브러리 초기화
var IMP = window.IMP; // 생략 가능
IMP.init("imp숫자"); // 발급받은 "가맹점 식별코드"를 사용
let menuName;
let totalPayMoney;
function orderContent(){
// 메뉴 이름(1~n개)하고 총 가격만 불러옴
const orderList = document.querySelectorAll('.orders');
let menuNames = [];
orderList.forEach((order) => {
// 각 주문에서 메뉴 이름 추출
const menuName = order.querySelector('.title').textContent;
menuNames.push(menuName);
});
menuName = menuNames[0];
if(menuNames.length > 1){
menuName = menuNames[0] + " 외 " + (menuNames.length-1) + "개";
}
// 총 가격 추출
const totalPayButton = document.querySelector('.submit.button').value;
totalPayMoney = totalPayButton.split("원")[0];
}
function requestPay() {
const contents = document.getElementById('contents');
orderContent();
// 결제 정보 준비
IMP.request_pay({
pg: "html5_inicis", // PG사
pay_method: "card", // 결제 수단
merchant_uid: "order_" + new Date().getTime(), // 주문번호
name: menuName, // 결제창에서 보여질 이름
amount: totalPayMoney, // 결제 금액
// buyer_email: "iamport@siot.do",
// buyer_name: "구매자이름",
// buyer_tel: "010-1234-5678", // 구매자 전화번호
// buyer_addr: "서울특별시 강남구 삼성동",
// buyer_postcode: "123-456", // 구매자 우편번호
// m_redirect_url: "/menu/order" // 모바일 결제 후 리디렉션될 URL
}, function (rsp) {
console.log(rsp);
if (rsp.success) {
// 결제 성공 시 로직
fetch("/menu/validImpUid", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"impUid": rsp.imp_uid, // Iamport 결제 고유 번호
"contents": contents.value, // 요청사항
"totalPrice": totalPayMoney
}),
})
.then(response => response.json())
.then(data => {
console.log(data);
// 서버에서 결제 검증 성공 후 리디렉션할 페이지로 이동
location.href = "/menu/order/" + data.orderId;
})
.catch(error => {
// 오류 처리 로직
alert("결제 검증에 실패했습니다. 다시 시도해주세요.");
});
} else {
// 결제 실패 시 로직,
alert("결제에 실패하였습니다. 에러 내용: " + rsp.error_msg);
}
});
}
menuController.java
결제 성공 시 데이터 저장 부분
이 부분 때문에 좀 저장 방식이 복잡해서 엔티티 두개 추가
@PostMapping("/validImpUid")
public ResponseEntity<?> order(@RequestBody PaymentValidationRequest validationRequest, HttpSession session) {
// 세션에서 주문 정보 및 관련 정보 가져오기
Long restaurantId = (Long)session.getAttribute("restaurantId");
int restaurantTableNo = (int)session.getAttribute("restaurantTableNo");
List<OrderDTO> ordersDTO = (List<OrderDTO>) session.getAttribute("orders");
if (ordersDTO == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("주문 정보가 없습니다.");
}
Orders newOrder = new Orders();
newOrder.setRestaurantTableNo(restaurantTableNo);
newOrder.setRestaurant(restaurantService.findById(restaurantId).orElseThrow(() -> new RuntimeException("Restaurant not found")));
newOrder.setPrice(validationRequest.getTotalPrice());
newOrder.setRegDate(LocalDateTime.now());
newOrder.setContent(validationRequest.getContents());
newOrder.setUid(validationRequest.getImpUid());
newOrder.setDivision(RoleOrders.WAIT);
List<OrderMenu> orderMenus = new ArrayList<>();
for (OrderDTO orderDTO : ordersDTO) {
OrderMenu orderMenu = new OrderMenu();
orderMenu.setMenuName(menuService.findById(orderDTO.getMenuId()).orElseThrow(() -> new RuntimeException("Menu not found")).getName());
orderMenu.setQuantity(orderDTO.getQuantity());
orderMenu.setPrice(menuService.findById(orderDTO.getMenuId()).get().getPrice()); // 기본 가격 설정
orderMenu.setOrder(newOrder); // 연결된 주문 설정
List<OrderOption> orderOptions = new ArrayList<>();
for (OrderDTO.OptionSelection selectedOption : orderDTO.getSelectedOptions()) {
OrderOption orderOption = new OrderOption();
MenuOption menuOption = menuOptionService.findById(selectedOption.getOptionId());
MenuOptionValue menuOptionValue = menuOptionValueService.findById(selectedOption.getValueId());
orderOption.setOptionName(menuOption.getContent() + ": " + menuOptionValue.getContent());
orderOption.setPrice(menuOptionValue.getPrice()); // 추가 가격 설정
orderOption.setOrderMenu(orderMenu); // 연결된 주문 메뉴 설정
orderOptions.add(orderOption); // 옵션 목록에 추가
}
orderMenu.setOrderOptions(orderOptions); // 주문 메뉴에 옵션 설정
orderMenus.add(orderMenu); // 주문 메뉴 목록에 추가
}
newOrder.setOrderMenus(orderMenus); // 주문에 주문 메뉴 목록 설정
Orders savedOrder = ordersService.add(newOrder); // 주문 저장
// 정상적인 처리 응답을 JSON 형태로 반환
Map<String, String> response = new HashMap<>();
response.put("message", "결제 검증 및 주문 정보 저장 성공.");
response.put("orderId", savedOrder.getId().toString());
return ResponseEntity.ok(response);
}
PaymentValidationRequest.java 이 부분을 따로 만들어서 클래스로 받아옴
package kr.ganjuproject.dto;
import lombok.Data;
//결제 검증 요청 데이터 클래스
@Data
public class PaymentValidationRequest {
private String impUid; // uid
private String contents; // 요구사항
private int totalPrice; // 총 금액
}
수정 및 새로 작성된 entity 부분
orders.java
package kr.ganjuproject.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Orders {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int restaurantTableNo;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderMenu> orderMenus = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "restaurant_id")
@ToString.Exclude
private Restaurant restaurant;
private int price;
private LocalDateTime regDate;
private String content;
@OneToOne(mappedBy = "order", orphanRemoval = true, cascade = CascadeType.ALL)
@ToString.Exclude
private Review review;
private String uid;
@Enumerated(EnumType.STRING)
private RoleOrders division;
}
OrderMenu.java
package kr.ganjuproject.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderMenu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String menuName;
private int quantity;
private int price; // 기본 가격 (1개 가격)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="orders_id")
@ToString.Exclude
private Orders order;
// 총 가격은 계산 필드로, 직접 저장하지 않고 메소드로 계산해서 제공할 수 있음
@Transient
public int getTotalPrice() {
// 옵션 가격을 포함한 총 가격 계산
int optionPrice = orderOptions.stream().mapToInt(OrderOption::getPrice).sum();
return (this.price + optionPrice) * this.quantity;
}
@OneToMany(mappedBy = "orderMenu", cascade = CascadeType.ALL, orphanRemoval = true)
@ToString.Exclude
private List<OrderOption> orderOptions = new ArrayList<>();
}
OrderOption.java
package kr.ganjuproject.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderOption {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String optionName; // 옵션 이름
private int price; // 옵션 추가 가격
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="order_menu_id")
private OrderMenu orderMenu;
}
이제 남은건 주문을 했을 때 웹소켓으로 메니저에게 정보 넘겨주는 부분