개인적인 사정으로 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;
}

 

 

이제 남은건 주문을 했을 때 웹소켓으로 메니저에게 정보 넘겨주는 부분

 

 

+ Recent posts