프로젝트 소개

2024. 03. 27 ~ 2024. 04. 22

프로젝트 개요

프로젝트 명

간주(간단한 주문)

기획 의도

요즘 식당에 가면 종종 볼 수 있는 테이블 키오스크를 대신하여 QR코드를 사용해서 손님 각자의 핸드폰으로 식당에 주문을 넣을 수 있다면 어떨까? 라는 생각에서 시작되었습니다.

가게에게는 키오스크 기기 대여 값의 부담을 덜어주고, 사용자에게는 기존 카드만 가능하던 테이블 키오스크 대신 여러 결제 방식을 제공해줌으로써 선택지를 넓혀줍니다.

팀원 구성

팀장 - 손지영 / 관리자

박윤재 / 유저

윤경재 / 매니저

 

github 링크

GitHub - Sonjiyo/Ganju

사이트  링크

http://ganju.pe.kr:8081/

 

1번식당 13번 테이블

 

프로젝트 기술 스택

사용 언어 및 기술

  • HTML : 웹 페이지의 구조를 정의하고, 콘텐츠를 웹에 표시하는 데 사용되는 기본 마크업 언어입니다.
  • CSS : 웹 페이지의 스타일을 지정하여, 사용자 인터페이스를 디자인하는 데 사용됩니다.
  • JavaScript : 사용자 인터랙션을 가능하게 하고, 동적인 웹 페이지를 만들기 위해 사용되는 스크립팅 언어입니다.
  • Java : 서버 사이드 로직과 데이터 처리를 위해 사용되며, 웹 애플리케이션의 백엔드 개발에 주로 사용됩니다.

개발 도구

  • Intellij
  • GitHub
  • Docker
  • Visual Studio

사용할 외부 API

  • 구글, 네이버, 카카오 로그인
  • 카카오 주소검색
  • 구글 이메일 인증
  • 아임포트 결제

핵심 비지니스 기능 목표

  • 관리자에게 승인 받은 가게는 매니저로 자격이 올라갑니다.
  • 매니저는 메뉴를 관리(추가, 삭제, 수정)할 수 있습니다.
  • 매니저는 손님의 주문을 받고, 취소할 수도 있습니다.
  • 손님은 QR코드로 접속한 사이트에서 메뉴를 주문할 수 있습니다.
  • 손님은 리뷰를 작성하고, 매니저는 리뷰를 수정할 수 있습니다.
  • 손님은 가게를 관리자에게 신고할 수 있습니다.
  • 관리자는 신고 내역을 확인하여 가게에 경고 조치를 할 수 있습니다.

노션 링크 - 프로젝트 진행과정이 다 적혀있음

https://www.notion.so/3-712c268f663b41ce96a2c81cea406f0b

 

발표 PPT

Ganju-중간발표

 

Ganju 최종 발표

 

docs.google.com

 

 

프로젝트 요구사항

 

팀 프로젝트-테이블오더

 

팀 프로젝트-테이블오더

요구사항 정의서 [요구사항 정의서] 구분,요구사항 ID,서비스,필요기능,기능설명 유저,USER-001,메뉴,상세보기,메뉴 선택시 해당 매뉴의 상세 보기 USER-002,옵션선택,옵션을 선택하면 가격변동 USER-00

docs.google.com

요구사항정의서

프로그램목록/Class

database ERD 다이어그램

 

정보구조도

테이블정의서

      •  

'공부 > Project' 카테고리의 다른 글

[JSP/SQL] 프로젝트 EYEVEL  (0) 2024.03.25
[HTTP/CSS/Java Script] Project Momizi  (0) 2024.03.24
[JAVA] 스도쿠 만들기 (Swing)  (0) 2024.03.20

에러 페이지와 

 

최종 테스트 qr 페이지

 

버그수정

 

에러 페이지

java

package kr.ganjuproject.controller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyErrorController implements ErrorController {

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request, Model model) {
        Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        String errorMessage = "알 수 없는 오류가 발생했습니다.";
        int statusCode = 0; // 초기 상태 코드 값 설정

        if (status != null) {
            statusCode = Integer.valueOf(status.toString());

            switch (HttpStatus.valueOf(statusCode)) {
                case NOT_FOUND:
                    errorMessage = "페이지를 찾을 수 없습니다.";
                    break;
                case INTERNAL_SERVER_ERROR:
                    errorMessage = "서버 내부 오류가 발생했습니다.";
                    break;
                case FORBIDDEN:
                    errorMessage = "접근이 금지되었습니다.";
                    break;
                case UNAUTHORIZED:
                    errorMessage = "인증이 필요합니다.";
                    break;
                case BAD_REQUEST:
                    errorMessage = "잘못된 요청입니다.";
                    break;
                case METHOD_NOT_ALLOWED:
                    errorMessage = "허용되지 않는 메소드입니다.";
                    break;
                // 여기에 더 많은 오류 상태를 추가할 수 있습니다.
                default:
                    errorMessage = "예기치 못한 오류가 발생했습니다.";
            }
        }

        model.addAttribute("statusCode", statusCode);
        model.addAttribute("errorMessage", errorMessage);

        // 모든 오류를 'error/errorPage.html' 템플릿을 통해 처리
        return "error/errorPage";
    }
}

 

html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${statusCode} + ' 에러'">오류</title>
    <link rel="stylesheet" href="/css/style.css">
    <link rel="stylesheet" href="/css/home/home.css">
    <style>
        h1, .button.home{
           margin: 30px 0;
        }
    </style>
</head>
<body>
<div class="innerBox">
    <div class="container">
        <div class="logo"><img src="/images/logo.svg" alt="logo"></div>
        <h1 th:text="${statusCode} + ' 에러'">오류 제목</h1>
        <p th:text="${errorMessage}">오류 메시지</p>
        <a th:href="@{/}" class="button home">홈으로 돌아가기</a>
    </div>
</div>
</body>
</html>

3주가 넘는 일정이지만 개인 사정이 생겨 길게 못한게 아쉬운

 

시간이 남을것 같아서 미니게임 하나 넣을까 했는데

 

기본 기능에 버그수정만으로 벅찬

 

이날은 간단한 중복클릭 방지와 비동기시 html을 지우고 다시 등록하다보니 이벤트 등록이 제대로 안된 부분들 수정

 

이래서 함수를 만들어서 쓰나보다 하고 깨닫는 날이었던

 

// 카테고리 버튼 색상 변경 이벤트
function setActiveCategory() {
    const categories = document.querySelectorAll('.category');
    const menuContainers = document.querySelectorAll('.menu-category');

    let currentActiveIndex = 0;
    menuContainers.forEach((container, index) => {
        const containerTop = container.getBoundingClientRect().top;
        if (containerTop - window.innerHeight / 2 < 0) {
            currentActiveIndex = index;
        }
    });
    categories.forEach((category, index) => {
        if (index === currentActiveIndex) {
            category.classList.add('active');
            ensureCategoryVisible(category);
        } else {
            category.classList.remove('active');
        }
    });
    // 카테고리 클릭 이벤트
    categories.forEach(category => {
        category.addEventListener('click', categoryClickListener);
    });
}

// 클릭한 카테고리가 화면에 완전히 보이지 않을 경우 스크롤
function ensureCategoryVisible(category) {
    const categoryRect = category.getBoundingClientRect();
    const containerRect = categoryContent.getBoundingClientRect();

    if (categoryRect.left < containerRect.left) {
        // 카테고리 버튼이 뷰포트 왼쪽 밖에 위치한 경우
        categoryContent.scrollLeft -= (containerRect.left - categoryRect.left) + 20; // 여백 추가
    } else if (categoryRect.right > containerRect.right) {
        // 카테고리 버튼이 뷰포트 오른쪽 밖에 위치한 경우
        categoryContent.scrollLeft += (categoryRect.right - containerRect.right) + 20; // 여백 추가
    }
}

// 카테고리 클릭 리스너를 위한 분리된 함수
let categoryBtn = true;
function categoryClickListener(e) {

    if(categoryBtn) {
        const targetId = e.currentTarget.getAttribute('data-target');
        const targetElement = document.getElementById(targetId);

        if (targetElement) {
            // category-content의 높이를 가져옵니다.
            const categoryContentHeight = document.querySelector('.category-content').offsetHeight;

            // targetElement까지의 절대 위치를 계산합니다.
            const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;

            // category-content의 높이만큼 위치를 조정합니다.
            const offsetPosition = elementPosition - categoryContentHeight;

            // 계산된 위치로 스크롤합니다.
            window.scrollTo({
                top: offsetPosition,
                behavior: "smooth"
            });
        }
        // 현재 클릭된 카테고리가 화면에 완전히 보이도록 스크롤 조정
        ensureCategoryVisible(e.currentTarget); // 여기서 this는 현재 클릭된 카테고리 요소입니다.

        categoryBtn = false;
        setTimeout( ()=>{
            categoryBtn = true;
        }, 1000);
    }
}

 

이런식으로 메서드로 묶어두고 지워지고 다시 만들 때마다 이벤트 등록

 

그리고 ppt 관련 준비를 한

 

화면 흐름도나

 

 

 

 

ER 다이어그램같은

 

것이나 

 

발표준비같은 짧은 작업을 함

 

팀장은 아니라 개인 구현 부분만 발표를 했는데

 

이 부분인데 생각보다 설명을 잘못하고 빼먹은것도 많아 수정할건 수정하고 대본을 만들어 리허설이 필요해 보였음

모바일에서 결재를 하려니 페이지 연결이나 콜백이 달라 구현

m_redirect_url: "http://localhost:8081/payment/verify" // 모바일 결제 후 리디렉션될 URL

 

pc 처럼 json으로 가격이나 uid나 다른 정보를 받아 오는게 아니고

get 방식으로 uid만 받아와서 이걸 또 iamport에 보내서 토큰 발급 받고 결재 금액 받아오고 검증하고 하는 절차가 따로 필요

 

PaymentController.java - 여기서 uid값을 일단 받고 session에 저장해둔 가격과 요구사항도 받아올 수 있음

    @GetMapping("/payment/verify")
    public String  verifyPayment(@RequestParam("imp_uid") String impUid, HttpSession session) throws IOException {
        // 세션에서 저장된 데이터 가져오기
        int totalPrice = (int) session.getAttribute("totalPrice");
        log.info("impUid = " + impUid);

        // 결제 검증 로직 수행
        boolean isPaymentValid = paymentService.verifyPayment(impUid, totalPrice);

        if (isPaymentValid) {        // 주문 생성 메서드 호출

            // 결제 검증 성공 시, 주문 생성 로직 수행
            PaymentValidationRequest validationRequest = new PaymentValidationRequest();
            validationRequest.setImpUid(impUid);

            OrderResponseDTO orderResponse = createOrders(validationRequest, session);
            messagingTemplate.convertAndSend("/topic/calls", orderResponse);
            return "redirect:/menu/order/" +orderResponse.getId();
        } else {
            // 결제 검증 실패 시, 결제 실패 페이지로 리디렉션
            return "redirect:/menu/main";
        }
    }

 

원래는 gson을 쓰다가 배포시에 버전관련 문제로 꼬여서 삭제하고 jackson 방식으로 변경

 

 

PaymentService.java -  이 부분에서 uid값과 토큰값을 받아 결재된 가격과 db 가격이 같은지 확인

 

그리고 getAccessToken()에서 yml에 저장해둔 api 키와 uid를 통해 토큰값을 받아온다

package kr.ganjuproject.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.ganjuproject.config.IamportConfig;
import kr.ganjuproject.dto.IamportDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentService {

    private final IamportConfig iamportConfig;
    private final ObjectMapper objectMapper; // ObjectMapper 인스턴스 주입

    // 인증 토큰 발급 받기
    public String getAccessToken() throws IOException {
        IamportDTO iDTO = iamportConfig.iamport();
        URL url = new URL("https://api.iamport.kr/users/getToken");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setDoOutput(true);

        // JSON 객체 생성 및 데이터 추가
        String jsonInputString = objectMapper.writeValueAsString(Map.of(
                "imp_key", iDTO.getApikey(),
                "imp_secret", iDTO.getSecret()
        ));

        try (OutputStream os = conn.getOutputStream()) {
            byte[] input = jsonInputString.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), "utf-8"))) {
            StringBuilder response = new StringBuilder();
            String responseLine;
            while ((responseLine = br.readLine()) != null) {
                response.append(responseLine.trim());
            }
            JsonNode jsonNode = objectMapper.readTree(response.toString());
            String accessToken = jsonNode.path("response").path("access_token").asText();
            return accessToken;
        }
    }

    public boolean verifyPayment(String impUid, int totalPrice) throws IOException {
        String accessToken = getAccessToken();
        RestTemplate restTemplate = new RestTemplate();
        String url = "https://api.iamport.kr/payments/" + impUid;

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

        HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);

        JsonNode responseObject = objectMapper.readTree(response.getBody());
        JsonNode responsePayment = responseObject.path("response");
        int paidAmount = responsePayment.path("amount").asInt();

        return paidAmount == totalPrice;
    }
}

 

그리고 환불 절차를 httpUrlConnection 방식으로 바꿈

다른방식 쓰다가 자꾸 간혈적으로 실패가 되서 바꾼 뒤로는 환불 잘 됨

환불이 되면 db 에서 order 를 삭제하고 식당 메니저에게 삭제 했다는 메시지를 날림

    // 환불 처리
    public Map<String, Object> requestRefund(Orders order, String reason, String accessToken) {
        try {
            URL url = new URL("https://api.iamport.kr/payments/cancel");
            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

            // HTTP 메서드 설정
            connection.setRequestMethod("POST");

            // 헤더 설정
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Authorization", accessToken);

            // 요청 본문 전송을 위해 출력 가능으로 설정
            connection.setDoOutput(true);

            // 요청 본문 구성
            String jsonInputString = "{\"imp_uid\": \"" + order.getUid() + "\", \"reason\": \"" + reason + "\"}";

            // 요청 본문 전송
            try (OutputStream os = connection.getOutputStream()) {
                byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
                os.write(input, 0, input.length);
            }

            // 응답 수신
            int responseCode = connection.getResponseCode();
            System.out.println("POST Response Code :: " + responseCode);

            if (responseCode == HttpURLConnection.HTTP_OK) { // 성공적인 응답 처리
                System.out.println("성공");
                ordersService.deleteOrder(order.getId());
                // 응답 본문을 JSON 객체로 파싱하고 필요한 정보를 추출/가공하여 반환
                OrderResponseDTO dto = ordersService.convertToOrderResponseDTO(order);

                messagingTemplate.convertAndSend("/topic/calls", order.getUid());
                return Map.of("success", true);
            } else {
                System.out.println("POST request not worked");
                // 오류 처리
                return Map.of("success", false);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return Map.of("error", e.getMessage());
        }
    }

 

결제 환불 및 헤더 부분 구현

 

유저의 어떤 페이지에서도 home과 장바구니로 가는 버튼을 만들고

결제 오류시 환불이나 주문쪽에서 환불을 할 수 있게 구현

환불처리 완료

그리고 header 부분 추가

관련 코드

bodyHEader.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<header th:fragment="bodyHeader(args)">
    <div>
        <a class="icon home-icon" th:if="${args.showIcon}" href='/menu/main'>
            <i class="fas fa-home"></i>
        </a>
    </div>
    <p th:text="${args.name}" class="center-text"></p>
    <div>
        <a class="icon cart-icon" th:if="${args.showBasket}" href='/menu/cart'>
            <i class="fas fa-shopping-cart"></i>
            <!-- 주문 개수 뱃지 -->
            <span class="cart-badge" th:if="${session.orders != null and #lists.size(session.orders) > 0}" th:text="${#lists.size(session.orders)}"></span>
        </a>
    </div>
</header>
</body>
</html>
 

 

style.css 에 헤더 부분 스타일 추가

/* menu 쪽 전체에서 아이콘 관련 */
.cart-icon {
       position: relative;
       display: inline-block;
   }
.cart-badge {
    position: absolute;
    top: -10px;
    right: -10px;
    padding: 2px 6px;
    border-radius: 50%;
    background: red;
    color: white;
    font-size: 12px;
}

 

그리고 환불 ( orderId값을 들고 와야 한다)

order.js

// 환불처리 스크립트
document.getElementById('refundButton').addEventListener('click', function() {
    const restaurantName = document.querySelector('.restaurant-name');
    const orderId = restaurantName.dataset.orderId;
    if (confirm('정말로 환불하시겠습니까?')) {
        fetch('/validRefund', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                orderId: orderId, // 서버 사이드에서 전달받은 주문 ID
            }),
        })
            .then(response => response.json())
            .then(data => {
                console.log(data.success);
                if (data.success) {
                    alert('환불 처리가 완료되었습니다.');
                    location.href = "/menu/main";

                    // 환불 처리 후 페이지 리디렉션 또는 UI 업데이트
                } else {
                    alert('환불 처리에 실패했습니다.');
                }
            })
            .catch(error => {
                console.error('Error:', error);
                alert('서버와의 통신 중 문제가 발생했습니다.');
            });
    }
});

 

PaymentController.java부분 ( 결제 관련은 menu와 orders에서 많이 옴김

    // 비동기 환불처리
    @PostMapping("/validRefund")
    public ResponseEntity<?> refundOrder(@RequestBody Map<String, Object> payload) throws IOException {
        String reason = "테스트용"; // 환불 사유
        Long orderId = Long.valueOf((String) payload.get("orderId"));

        Orders order = ordersService.findById(orderId).get();

        System.out.println("order" + order);
        String accessToken = refundService.getAccessToken();
        Map<String, Object> refundResult = refundService.requestRefund(order.getUid(), reason, accessToken);

        System.out.println("refundResult" + refundResult);
        System.out.println("order" + order);

        // "success" 키의 값이 true인지 확인하여 성공 여부를 판단
        Boolean success = (Boolean) refundResult.get("success");
        if (Boolean.TRUE.equals(success)) { // 성공 여부 확인
            return ResponseEntity.ok().body(Map.of("success", true, "message", "환불 처리가 완료되었습니다."));
        } else {
            return ResponseEntity.badRequest().body(Map.of("success", false, "message", "환불 처리에 실패했습니다."));
        }
    }

 

검증 부분 - 토큰값 받아오고 맞으면 환불하고 db 삭제

PaymentService.java

package kr.ganjuproject.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.ganjuproject.config.IamportConfig;
import kr.ganjuproject.dto.IamportDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentService {

    private final IamportConfig iamportConfig;
    private final ObjectMapper objectMapper; // ObjectMapper 인스턴스 주입

    // 인증 토큰 발급 받기
    public String getAccessToken() throws IOException {
        IamportDTO iDTO = iamportConfig.iamport();
        URL url = new URL("<https://api.iamport.kr/users/getToken>");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setDoOutput(true);

        // JSON 객체 생성 및 데이터 추가
        String jsonInputString = objectMapper.writeValueAsString(Map.of(
                "imp_key", iDTO.getApikey(),
                "imp_secret", iDTO.getSecret()
        ));

        try (OutputStream os = conn.getOutputStream()) {
            byte[] input = jsonInputString.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), "utf-8"))) {
            StringBuilder response = new StringBuilder();
            String responseLine;
            while ((responseLine = br.readLine()) != null) {
                response.append(responseLine.trim());
            }
            JsonNode jsonNode = objectMapper.readTree(response.toString());
            String accessToken = jsonNode.path("response").path("access_token").asText();
            return accessToken;
        }
    }

    public boolean verifyPayment(String impUid, int totalPrice) throws IOException {
        String accessToken = getAccessToken();
        RestTemplate restTemplate = new RestTemplate();
        String url = "<https://api.iamport.kr/payments/>" + impUid;

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

        HttpEntity entity = new HttpEntity<>("parameters", headers);
        ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);

        JsonNode responseObject = objectMapper.readTree(response.getBody());
        JsonNode responsePayment = responseObject.path("response");
        int paidAmount = responsePayment.path("amount").asInt();

        return paidAmount == totalPrice;
    }
}

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

 

 

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

 

 

하루 늦은 기록 

 

프론트 디자인 및 이벤트 구현 완료

 

지금은 menu와 카테고리 더미데이터만 불러와서 테스트 해봤는데

 

다음주에는 결재하고 인증키 까지 받아서 db 에 저장하고 환불 하는 기능까지 해볼 예정

 

main 부분이 많이 바뀌진 않았지만 버그 수정 및 팀장님이 말하던 아이콘 위치나 이벤트는 완료

 

main.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>
    <link href="/css/style.css" rel="stylesheet">
    <link href="/css/user/main.css" rel="stylesheet" type="text/css">
    <script src="/js/user/main.js" type="text/javascript" defer></script>
</head>
<body>
<div class="innerBox">
    <header></header>
    <div class="container">
        <div class="restaurant-image">
            <img src="/images/sample.png" alt="식당 메인 이미지" class="restaurant-img">
        </div>
        <div class="title">
            <div class="restaurant-name">더조은 식당</div>
            <button class="report-button button">
                <i class="fas fa-exclamation-triangle"></i>신고하기
            </button>
        </div>
        <div class="ratings-reviews">
            <div class="ratings">
                <div class="stars">
                    <span><i class="far fa-star"></i></span>
                    <span><i class="far fa-star"></i></span>
                    <span><i class="far fa-star"></i></span>
                    <span><i class="far fa-star"></i></span>
                    <span><i class="far fa-star"></i></span>
                </div>  <!-- 5개의 별을 기본으로 설정 -->
                <label>3.8</label> <!-- 예시 평점 -->
            </div>
            <button class="call-button button">호출하기</button>
        </div>
        <div class="buttons">
            <button class="menu-btn button tab-btn" data-target="menu-content">메뉴</button>
            <button class="announcement-btn button tab-btn" data-target="announcement-content">공지사항</button>
            <button class="reviews-btn button tab-btn" data-target="review-content">리뷰</button>
        </div>
        <div class="content">
            <div class="menu-content tab-content">
                <div class="category-content">
                    <div th:each="category : ${categories}" class="category" th:text="${category.name}" th:data-targets="'menu-category-' + ${category.id}"></div>
                </div>
                <div class="menus-container">
                    <div th:each="category : ${categories}" class="menu-category"  th:id="'menu-category-' + ${category.id}" th:data-category-id="${category.id}" >
                        <div th:text="${category.name}" class="category-name"></div>
                        <div class="menus" th:each="menu : ${menus}">
                            <div th:if="${menu.category.id} == ${category.id}" class="menu" th:onclick="'location.href=\'/menu/info?id=' + ${menu.id} + '\''">
                                <div class="image">
                                    <img src="/images/sample.png" alt="메뉴 이미지" class="restaurant-image">
                                </div>
                                <div class="text">
                                    <div th:text="${menu.name}" class="menu-name"></div>
                                    <!--th:text="${menu.info}"-->
                                    <div class="menu-info">맛있음</div>
                                    <div th:text="${menu.price + '원'}" class="menu-price"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="announcement-content tab-content">
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항1</div>
                        <div class="date">16:12</div>
                    </div>
                    <div class="content">쉬고싶어서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항2</div>
                        <div class="date">2024.04.03</div>
                    </div>
                    <div class="content">아파서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항3</div>
                        <div class="date">2024.04.02</div>
                    </div>
                    <div class="content">싱숭생숭해서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항4</div>
                        <div class="date">2024.04.01</div>
                    </div>
                    <div class="content">해어져서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항5</div>
                        <div class="date">2024.03.20</div>
                    </div>
                    <div class="content">그냥 쉽니다</div>
                </div>
            </div>
            <div class="review-content tab-content">
                <div class="review">
                    <div class="title">박윤재</div>
                    <div class="mid">
                        <!-- 여기에 별점을 표시할 div를 추가합니다. 데이터 속성(data-rating)을 사용하여 별점을 지정합니다. -->
                        <div class="star" data-rating="5"></div>
                        <div class="date">1달전</div>
                    </div>
                    <div class="content">음식이 친절하고 사장님이 맛있어요</div>
                </div>
                <div class="review">
                    <div class="title">손지영</div>
                    <div class="mid">
                        <div class="star" data-rating="4"></div>
                        <div class="date">2024.04.01</div>
                    </div>
                    <div class="content">1주전</div>
                </div>
                <div class="review">
                    <div class="title">윤경재</div>
                    <div class="mid">
                        <div class="star" data-rating="1"></div>
                        <div class="date">2시간전</div>
                    </div>
                    <div class="content">집에 보내주세요</div>
                </div>
            </div>
        </div>
    </div>
    <footer></footer>
    <template  th:replace="~{user/modal/modal :: modal}" ></template>
</div>
</body>
</html>

 

main.css

@charset "UTF-8";

/* 메인 이미지 */
.container .restaurant-image{
    width: 100%;
    height: 200px;
    overflow: hidden;
}

.container .restaurant-image img{
    width: 100%;
    object-fit: cover;
    object-position: center top;
}

/* 타이틀과 신고버튼 */
.container .title{
    width: 100%;
    padding: var(--padding);
    display: flex;
    justify-content: space-between;
    align-items: center;
}

/* 타이틀 */
.container .title .restaurant-name{
    font-size: var(--font-big);
    font-weight: bold;
}

/* 신고 */
.container .title .report-button{
    padding: 0;
    background-color: transparent;
    color: var(--red);
    border: none;
    font-size: var(--font-small);
}

.container .title .report-button .fas{
    margin-right: 5px;
}

/* 별표와 리뷰 버튼*/
.container .ratings-reviews{
    margin-bottom: 10px;
    padding: var(--padding);
    padding-top: 0;
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

/* 별표 부분*/
.container .ratings-reviews .ratings {
    display: flex;
    align-items: center;
}

/* 별표의 별 부분 */
.container .ratings-reviews .ratings .stars{
    position: relative;
    display: inline-block;
    font-size: var(--font-content); /* 별 크기 조정 */
    direction: ltr; /* 별을 왼쪽에서 오른쪽으로 채워나가기 위해 */
}

/* 별표의 숫자 부분 */
.container .ratings-reviews .ratings label{
    margin-left: 10px;
}

/* 호출 버튼 부분*/
.container .ratings-reviews .call-button{
    border: 0;
    border-radius: 0;
    padding: 10px 30px;
}

/* 메뉴 공지사항 리뷰 */
.container .buttons{
    border-bottom: 3px solid var(--orange);
    display: flex;
    width: 100%;
}

/* 메뉴 */
.container .buttons .menu-btn{
    border: 0;
    border-radius: 0;
    flex: 1;
    font-size: var(--font-content);
    padding: var(--padding);
}

/* 공지사항 */
.container .buttons .announcement-btn{
    background-color: inherit;
    color: var(--gray);
    border: 0;
    border-radius: 0;
    flex: 1;
    font-size: var(--font-content);
    padding: var(--padding);
}

/* 리뷰 */
.container .buttons .reviews-btn{
    background-color: inherit;
    color: var(--gray);
    border: 0;
    border-radius: 0;
    flex: 1;
    font-size: var(--font-content);
    padding: var(--padding);
}

/* 버튼을 눌렀을때 보여주는 콘텐츠 영역 */
.container .content{
    width: 100%;
}

/* 메뉴 콘텐츠 */
.container .content .menu-content{
    width: 100%;
}

/* 카테고리 슬라이더 부분 */
.container .content .menu-content .category-content{
    display: flex; /* 이 부분을 추가합니다 */
    overflow-x: auto; /* 수평 스크롤을 위해 추가합니다 */
    background-color: var(--white);
    scrollbar-width: none;
    z-index: 1; /* 다른 요소들 위에 오도록 z-index 설정 */
}

.sticky {
    position: fixed;
    top: 0; /* 또는 header 높이에 맞추어 조정 */
    width: var(--innerBox-width);
    box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* 선택사항: 고정된 요소에 그림자 효과 추가 */
}

.container .content .menu-content .category-content .category{
    margin: 10px;
    padding: 10px 20px;
    flex: 0 0 auto; /* flex 항목이 자신의 크기를 유지하도록 설정합니다 */
    border-radius: 20px;
    text-align: center;
    background-color: var(--white);
    color:var(--black);
    cursor: pointer; /* 마우스 오버 시 커서 변경을 위해 추가합니다 */
    border: 1px solid var(--gray);
    transition: background-color 0.3s, color 0.3s; /* 부드러운 색상 전환 */
}
.container .content .menu-content .category-content .category.active{
    background-color: var(--gray);
    color: var(--white);
}
.container .content .menu-content .category-content:last-child{
    margin-right: 0;
}

/* 메뉴 리스트 */
.container .content .menu-content .menus-container {
}
.container .content .menu-content .menus-container .category-name{
    padding:var(--padding);
    background-color: var(--extra-light-gray);
    color: var(--gray);
    font-size: var(--font-middle);
    font-weight: bold;
    border-bottom: 2px solid var(--light-gray);
}
.container .content .menu-content .menus-container .menu{
    width: 100%;
    display: flex;
    border-bottom: 2px solid var(--light-gray);
    cursor: pointer;
}
.container .content .menu-content .menus-container .menu .image{
    padding: 10px;
    width: 120px;
    height: 120px;
    overflow: hidden;
}
.container .content .menu-content .menus-container .menu .image img{
    width: 100%;
    height: 100%;
}
.container .content .menu-content .menus-container .menu .text{
    padding: 10px;
    display: flex;
    flex-direction: column;
}
.container .content .menu-content .menus-container .menu .text .menu-name{
    margin-bottom: 10px;
    font-size: var(--font-content);
    font-weight: bold;
    color:var(--black);
}
.container .content .menu-content .menus-container .menu .text .menu-info{
    margin-bottom: 10px;
    flex: 1;
    font-size: var(--font-content);
    color:var(--gray);
}
.container .content .menu-content .menus-container .menu .text .menu-price{
    font-size: var(--font-content);
    color:var(--black);
}

/* 공지사항 콘텐츠 */
.container .content .announcement-content{
    width: 100%;
    display: none;
}
.container .content .announcement-content .announcement {
    width: 100%;
    display: flex;
    flex-direction: column;
    padding: 20px;
    border-bottom: 2px solid var(--light-gray);
}
.container .content .announcement-content .announcement .title {
    padding:0;
    font-size: var(--font-middle);
    color: var(--black);
    margin-bottom: 20px;
}
.container .content .announcement-content .announcement .date{
    padding:0;
    font-size: var(--font-small);
    color: var(--light-gray);
}
.container .content .announcement-content .announcement .content{
    padding:0;
    font-size: var(--font-content);
    color: var(--gray);
}

/* 리뷰 */
.container .content .review-content{
    width: 100%;
    display: none;
    padding: var(--padding);
}
.container .content .review-content .review{
    margin-bottom: 20px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    border: 2px solid var(--light-gray);
    padding: var(--padding);
}
.container .content .review-content .review .title{
    padding: 0;
    font-size: var(--font-content);
    color: var(--black);
    margin-bottom: 5px;
}
.container .content .review-content .review .mid{
    display: flex;
    font-size: var(--font-small);
    color: var(--light-gray);
    flex: 1;
    padding-bottom: 20px;
}
.container .content .review-content .review .mid .star span{
    font-size: var(--font-small);
}
.container .content .review-content .review .mid .star span .fas {
    color: var(--gold); /* 노란색 별 */
}
.container .content .review-content .review .mid .star span .far{
    color: var(--light-gray); /* 회색 별 */
}
.container .content .review-content .review .mid .date{
    margin-left: 10px;
}
.container .content .review-content .review .content{
    font-size: var(--font-content);
    color: var(--black);
}

 

main.js

/* 식당 평점 별 부분*/
function ratingStar(){
    const rating = document.querySelector('.ratings');
    const stars = rating.querySelectorAll('.stars span i');
    const label = rating.querySelector('label');
    const ratingValue = parseFloat(label.textContent);

    // 별 아이콘 초기화
    stars.forEach(star => {
        star.classList.remove('fas', 'fa-star', 'fa-star-half-alt');
        star.classList.add('far', 'fa-star');
        star.style.color = 'var(--light-gray)'; // 기본 별 색상
    });

    // 평점에 따라 별 아이콘 적용
    for (let i = 0; i < stars.length; i++) {
        if ((ratingValue - i) > 1 ) {
            stars[i].classList.remove('far', 'fa-star-half-alt');
            stars[i].classList.add('fas', 'fa-star');
            stars[i].style.color = 'var(--gold)'; // 채워진 별 색상
        }
        else if((ratingValue - i) >= 0.5){
            stars[i].classList.remove('far', 'fa-star');
            stars[i].classList.add('fas', 'fa-star-half-alt');
            stars[i].style.color = 'var(--gold)'; // 반 채워진 별 색상
            break;
        }
    }
}

/* 메뉴 공지 사항 리뷰 버튼 관련 부분 */
const buttons = document.querySelectorAll(".tab-btn");
const contents = document.querySelectorAll(".tab-content");

// 탭 버튼 클릭 이벤트 핸들러
function handleTabClick(event) {
    // 모든 버튼과 컨텐츠 초기화
    buttons.forEach(btn => {
        btn.style.backgroundColor = 'inherit';
        btn.style.color = '#717171';
    });

    contents.forEach(content => {
        content.style.display = 'none';
    });

    // 클릭된 버튼과 관련된 컨텐츠 활성화
    const selectedContentId = event.target.getAttribute('data-target');
    const selectedContent = document.querySelector(`.${selectedContentId}`);

    console.log(selectedContentId);
    event.target.style.backgroundColor = 'var(--orange)';
    event.target.style.color = 'var(--white)';
    selectedContent.style.display = 'block';
}

// 각 버튼에 이벤트 리스너 추가
buttons.forEach(button => {
    button.addEventListener('click', handleTabClick);
});


// 슬라이더 부분
const slider = document.querySelector('.category-content');
let isDown = false;
let startX;
let scrollLeft;

slider.addEventListener('mousedown', (e) => {
    isDown = true;
    slider.classList.add('active');
    startX = e.pageX - slider.offsetLeft;
    scrollLeft = slider.scrollLeft;
});

slider.addEventListener('mouseleave', () => {
    isDown = false;
    slider.classList.remove('active');
});

slider.addEventListener('mouseup', () => {
    isDown = false;
    slider.classList.remove('active');
});

slider.addEventListener('mousemove', (e) => {
    if(!isDown) return;
    e.preventDefault();
    const x = e.pageX - slider.offsetLeft;
    const walk = (x - startX) * 3; //scroll-fast
    slider.scrollLeft = scrollLeft - walk;
});

// 현재 활성화된 카테고리를 갱신하고 배경과 글씨 색 변견
const categories = document.querySelectorAll('.category');
const menuContainers = document.querySelectorAll('.menu-category');
const categoryContent = document.querySelector('.category-content');

// 카테고리 버튼 색상 변경 이벤트
function setActiveCategory() {
    let currentActiveIndex = 0;
    menuContainers.forEach((container, index) => {
        const containerTop = container.getBoundingClientRect().top;
        if (containerTop - window.innerHeight / 2 < 0) {
            currentActiveIndex = index;
        }
    });

    categories.forEach((category, index) => {
        if (index === currentActiveIndex) {
            category.classList.add('active');
            ensureCategoryVisible(category);
        } else {
            category.classList.remove('active');
        }
    });
}

// 클릭한 카테고리가 화면에 완전히 보이지 않을 경우 스크롤
function ensureCategoryVisible(category) {
    const categoryRect = category.getBoundingClientRect();
    const containerRect = categoryContent.getBoundingClientRect();

    console.log(category);
    console.log("들어는 오니");
    if (categoryRect.left < containerRect.left) {
        // 카테고리 버튼이 뷰포트 왼쪽 밖에 위치한 경우
        categoryContent.scrollLeft -= (containerRect.left - categoryRect.left) + 20; // 여백 추가
    } else if (categoryRect.right > containerRect.right) {
        // 카테고리 버튼이 뷰포트 오른쪽 밖에 위치한 경우
        categoryContent.scrollLeft += (categoryRect.right - containerRect.right) + 20; // 여백 추가
    }
}

// 카테고리 클릭 이벤트
categories.forEach(category => {
    category.addEventListener('click', () => {
        const targetId = this.getAttribute('data-targets');
        const targetElement = document.getElementById(targetId);

        if (targetElement) {
            // category-content의 높이를 가져옵니다.
            const categoryContentHeight = document.querySelector('.category-content').offsetHeight;

            // targetElement까지의 절대 위치를 계산합니다.
            const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;

            // category-content의 높이만큼 위치를 조정합니다.
            const offsetPosition = elementPosition - categoryContentHeight;

            // 계산된 위치로 스크롤합니다.
            window.scrollTo({
                top: offsetPosition,
                behavior: "smooth"
            });
        }
        // 현재 클릭된 카테고리가 화면에 완전히 보이도록 스크롤 조정
        ensureCategoryVisible(this); // 여기서 this는 현재 클릭된 카테고리 요소입니다.
    });
});

// 페이지 로드 시 .category-content의 원래 위치를 계산하여 저장
const stickyElement = document.querySelector('.category-content');
const headerOffset = document.querySelector('header').offsetHeight; // 만약 header가 있다면
const originalOffsetTop = stickyElement.offsetTop - headerOffset; // header 높이를 고려한 조정값

// 스크롤 이벤트 리스너를 추가하는 새 함수
window.addEventListener('scroll', () => {
    if (window.pageYOffset >= originalOffsetTop ) {
        stickyElement.classList.add('sticky');
    } else {
        stickyElement.classList.remove('sticky');
    }
});

// 리뷰 별점 정수값을 별 개수로 표시하는
function reviewStar(){
    const reviewStars = document.querySelectorAll('.review .star');

    reviewStars.forEach(star => {
        const rating = parseInt(star.getAttribute('data-rating'));
        const totalStars = 5;
        let starsHtml = '';

        // 채워진 별 생성
        for (let i = 1; i <= rating; i++) {
            starsHtml += '<span><i class="fas fa-star"></i></span>';
        }

        // 빈 별 생성
        for (let i = rating + 1; i <= totalStars; i++) {
            starsHtml += '<span><i class="far fa-star"></i></span>';
        }

        // 별점을 HTML에 삽입
        star.innerHTML = starsHtml;
    });
}

document.addEventListener('DOMContentLoaded', () => {
    // 별포 표시 부분 함수
    ratingStar();

    // 리뷰 콘텐츠의 별점 개수 표시 하는 함수
    reviewStar();

    // 스크롤 이벤트
    window.addEventListener('scroll', setActiveCategory);

    // 초기 활성화 카테고리 설정
    setActiveCategory();
});

 

그리고 코드가 너무 길어져 modal 부분을 분리했다

modal.html

<!DOCTYPE html>
<html lang="en">
<div th:fragment="modal">
    <link href="/css/user/modal/modal.css" rel="stylesheet" type="text/css">
    <script src="/js/user/modal/modal.js" type="text/javascript" defer></script>
    <!-- 신고하기 모달 창 -->
    <div id="reportModal" class="modal report-modal">
        <div class="modal-content">
            <h2 class="modal-title">가게 신고</h2>
            <p class="modal-warning">가게 신고 시 돌이킬 수 없으며,<br>허위로 신고를 작성한 경우 삭제됩니다.</p>
            <label for="reportReason" class="modal-label">신고 사유</label>
            <textarea id="reportReason" class="modal-input"></textarea>
            <button class="modal-submit button" id="reportCall">신고하기</button>
        </div>
    </div>
    <!-- 호출하기 모달 창 -->
    <div id="callModal" class="modal call-modal">
        <div class="modal-content">
            <h2 class="modal-title">호출하기</h2>
            <form id="callForm">
                <label><input type="radio" class="check" name="callOption" value="water" checked/><label></label><span>물 주세요</span></label>
                <label><input type="radio" class="check" name="callOption" value="cup" /><label></label><span>컵 주세요</span></label>
                <label><input type="radio" class="check" name="callOption" value="towel" /><label></label><span>물수건 주세요</span></label>
                <label><input type="radio" class="check" name="callOption" value="staff" /><label></label><span>그냥 직원 오세요</span></label>
                <input type="button" class="modal-submit button" id="submitCall" value="호출하기">
            </form>
        </div>
    </div>
</div>
</html>

 

modal.css


/* 모달 창 스타일 */
.modal {
    display: none; /* 초기에는 숨김 */
    position: absolute; /* 화면 중앙에 위치 */
    z-index: 1; /* 내용 위에 표시 */
    left: 0;
    top: 0;
    width: 100%; /* 전체 너비 */
    height: 100%; /* 전체 높이 */
    overflow: auto; /* 내용이 넘칠 경우 스크롤 */
    background-color: rgb(0,0,0); /* 검은색 배경 */
    background-color: rgba(0,0,0,0.4); /* 어두운 투명도 */
}

/* 모달 내용 스타일 */
.modal-content {
    background-color: #ffffff;
    margin: 10% auto; /* 페이지 중앙에 위치 조정 */
    padding: 20px; /* 패딩 증가 */
    border-radius: 8px; /* 모서리 둥글게 처리 */
    width: 400px; /* 너비 조정 */
    max-width: 90%; /* 최대 너비 설정 */
}

.modal-title {
    font-size: var(--font-middle); /* 글씨 크기 조정 */
    color: var(--black); /* 글씨 색상 변경 */
    margin-bottom: 20px; /* 하단 여백 추가 */
}

.modal-warning {
    color: var(--red); /* 경고 문구 색상 변경 */
    font-size: 16px; /* 글씨 크기 조정 */
    margin-bottom: 20px; /* 하단 여백 추가 */
}

.modal-label {
    display: block; /* 라벨을 블록 요소로 변경 */
    margin-bottom: 10px; /* 여백 추가 */
    font-weight: bold; /* 글씨 두껍게 */
    color: var(--black); /* 글씨 색상 변경 */
}

.modal-input {
    width: 100%;
    height: 100px;
    border: 1px solid var(--light-gray);
    border-radius: 4px; /* 모서리 둥글게 처리 */
    padding: 10px; /* 내부 패딩 추가 */
    margin: 0 0 20px 0; /* 하단 여백 추가 */
}

.modal-submit {
    width: 100%;
    font-size: var(--font-content);
}

/* 호출하기 모달 */
#callForm {
    width: 100%;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
}

#callForm>label {
    display: flex; /* 라벨 블록 처리 */
    align-items: center;
    margin-bottom: 10px; /* 라벨 간 간격 */
    cursor: pointer; /* 마우스 오버 시 커서 변경 */
}

#callForm>.modal-submit{
    margin-top: 20px;
}
/* 옵션 사용자 버튼 */
.check{
    display: none;
}

/* 라벨의 가상 요소를 이용해 라디오 버튼 스타일링 */
#callForm .check[type="radio"] + label::before {
    content: '\f111'; /* Font Awesome의 circle 아이콘 */
    font-family: 'Font Awesome 5 Free';
    font-weight: 500;
    color: var(--light-gray);
    font-size: var(--font-middle);
    margin-right: var(--padding);
}

/* 체크된 라디오 버튼의 스타일 */
#callForm .check[type="radio"]:checked + label::before {
    content: '\f058'; /* Font Awesome의 check-circle 아이콘 */
    color: var(--orange);
}

 

modal.js


// 신고하기 버튼 클릭 이벤트
document.querySelector('.report-button').addEventListener('click', () => {
    action();
    document.getElementById('reportModal').style.display = 'block';
});

// 신고하기 버튼 클릭 이벤트로 모달 창 닫기 (옵션)
document.querySelector('.modal-submit').addEventListener('click', () => {
    document.getElementById('reportModal').style.display = 'none';
    document.body.style.overflow = ''; // 스크롤 활성화
});

// 호출하기 버튼 클릭 이벤트
document.querySelector('.call-button').addEventListener('click', () => {
    action();
    document.getElementById('callModal').style.display = 'block';
});

// 호출하기 모달에서 호출하기 버튼 클릭 이벤트
document.getElementById('submitCall').addEventListener('click', () => {
    // 실제 애플리케이션에서는 이곳에 선택된 옵션을 처리하는 로직을 구현합니다.
    // 예: 선택된 라디오 버튼의 값을 서버로 전송
    console.log('호출 옵션:', document.querySelector('input[name="callOption"]:checked').value);
    document.getElementById('callModal').style.display = 'none'; // 모달 닫기
    document.body.style.overflow = ''; // 스크롤 활성화
});

// 모달 창 밖을 클릭할 때 모달 창 닫기
window.addEventListener('click', e => {

    if (e.target.classList.contains('modal')) {
        e.target.style.display = 'none';
        document.body.style.overflow = ''; // 스크롤 활성화
    }
});

function action(){
    window.scrollTo(0, 0); // 스크롤을 맨 위로 이동
    document.body.style.overflow = 'hidden'; // 스크롤 비활성화
}

 

주말에 스프링 공부와 정보처리기사 실기 자격증 공부 예정

'공부 > Ganju' 카테고리의 다른 글

[Spring/AWS] 팀프로젝트 10일차  (0) 2024.04.09
[Spring/AWS] 팀프로젝트 9일차  (0) 2024.04.08
[Spring/AWS] 팀프로젝트 7일차  (0) 2024.04.04
[Spring/AWS] 팀프로젝트 6일차  (0) 2024.04.03
[Spring/AWS] 팀프로젝트 5일차  (0) 2024.04.02

오늘은 이번주 금요일 중간 발표를 위해 보여지는 페이지를 먼저 구현하기로 했습니다.

 

손님이 처음 보는 메뉴 메인 페이지 구현

상단에 가게 메인 이미지

두번째 타이틀 신고 버튼

세번째 평점 평균, 리뷰

네번재 호출하기 버튼

다섯번째 메뉴 공지 리뷰 버튼 해당 버튼을 클릭하면 여섯번째에 거기에 해당하는 컨텐츠를 보여준다

메뉴 버튼을 클락하면 나오는 메뉴 컨텐츠의 첫번째는 카테고리를 보여주고 두번째는 메뉴를 카테고리로 분류해서 세로로 보여준다

보여주기 전에 더미 db 파일이 필요하다

package kr.ganjuproject.repository.dumi;

import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import kr.ganjuproject.entity.Category;
import kr.ganjuproject.entity.Menu;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;

@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMenuDB {
    private final InitMenuService initMenuService;
    @PostConstruct  // 생성자 실행되면 바로 호출
    public void init() {
        initMenuService.init();
    }
    @Component
    static class InitMenuService {
        @Autowired
        EntityManager em;
        @Transactional
        public void init() {
            // 카테고리 데이터 생성
            String[] categoryNames = {"한식", "중식", "일식", "양식", "분식", "디저트"};
            for (int i = 0; i < categoryNames.length; i++) {
                Category category = new Category(null, i + 1, categoryNames[i], new ArrayList<>());
                em.persist(category);

                // 각 카테고리에 메뉴 추가
                for (int j = 0; j < 5; j++) {
                    Menu menu = new Menu();
                    menu.setName(category.getName() + " 메뉴 " + (j + 1));
                    menu.setPrice((j + 1) * 1000); // 가격 설정 예시
                    menu.setCategory(category); // 생성된 카테고리에 메뉴 설정
                    // 메뉴에 대한 추가적인 설정...

                    em.persist(menu);

                    // 카테고리 객체에 메뉴 객체 연결 (양방향 관계 설정)
                    category.getMenus().add(menu);
                }
            }
            System.out.println("더미 데이터 생성 완료");
        }
    }
}

이렇게 서버를 시작할때 더미데이터를 만드려면

@Profile("local")

이 부분과 yml 파일에

spring:
  output:
    ansi:
      enabled: always
  profiles:
    active: local

active 를 지정해줘야 한다

어쩌다보니 기능까지 같이 구현해 버림

메인 더미데이터로 카테고리 슬라이더 메뉴 펼치기 공지사항 더미 리뷰 더미

그리고 메뉴 클릭시 페이지링크 까지 구현

오늘 작성한 코드 변경시 참고하기위해 남깁니다

 

main.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</title>
    <link href="/css/style.css" rel="stylesheet">
    <link href="/css/user/main.css" rel="stylesheet" type="text/css">
    <script src="/js/user/main.js" type="text/javascript" defer></script>

    <!-- 슬릭 슬라이더 관련 -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.min.js"></script>

    <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.css" />
    <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick-theme.css" />
</head>
<body>
<div class="innerBox">
    <header></header>
    <div class="container">
        <img src="/images/sample.png" alt="식당 메인 이미지" class="restaurant-image">
        <div class="title">
            <div class="restaurant-name">식당 이름</div>
            <button class="report-button button">신고</button>
        </div>
        <div class="ratings-reviews">
            <div class="ratings">
                <div class="stars">★★★★☆</div>
                <label>0.0</label>
            </div>
            <button class="review-button button">리뷰</button>
        </div>
        <div class="call">
            <button class="call-button button">호출하기</button>
        </div>
        <div class="buttons">
            <button class="menu-btn button">메뉴</button>
            <button class="announcement-btn button">공지사항</button>
            <button class="reviews-btn button">리뷰</button>
        </div>
        <div class="content">
            <div class="menu-content">
                <div class="category-content">
                    <div th:each="category : ${categories}" class="category">
                        <div th:text="${category.name}" class="category-name"></div>
                    </div>
                </div>
                <div class="menus-container">
                    <div th:each="category : ${categories}" class="menu-category" th:data-category-id="${category.id}">
                        <div th:text="${category.name}" class="category-name"></div>
                        <div class="menus" th:each="menu : ${menus}">
                            <div th:if="${menu.category.id} == ${category.id}" class="menu" onclick="location.href='/menu/info?id=${menu.id})}'">
                                <div class="image"><img src="/images/sample.png" alt="메뉴 이미지" class="restaurant-image"></div>
                                <div class="text">
                                    <div th:text="${menu.name}" class="menu-name"></div>
                                    <div th:text="${menu.info}" class="menu-info"></div>
                                    <div th:text="${menu.price}" class="menu-price"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="announcement-content">
                <div class="announcement">
                    <div class="title">공지사항1</div>
                    <div class="date">2024.04.01</div>
                    <div class="content">쉬고싶어서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">공지사항2</div>
                    <div class="date">2024.04.02</div>
                    <div class="content">여행 가고싶어서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">공지사항3</div>
                    <div class="date">2024.04.03</div>
                    <div class="content">아플것 같아서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">공지사항4</div>
                    <div class="date">2024.04.04</div>
                    <div class="content">그냥 쉽니다</div>
                </div>
            </div>
            <div class="review-content" >
                <div class="review">
                    <div class="title">박윤재</div>
                    <div class="mid">
                        <div class="star">★★★★★</div>
                        <div class="date">16:00</div>
                    </div>
                    <div class="content">음식이 친절하고 사장님이 맛있어요</div>
                </div>
                <div class="review">
                    <div class="title">손지영</div>
                    <div class="mid">
                        <div class="star">★★★☆☆</div>
                        <div class="date">2024.04.01</div>
                    </div>
                    <div class="content">음 괜찮네요</div>
                </div>
                <div class="review">
                    <div class="title">윤경재</div>
                    <div class="mid">
                        <div class="star">☆☆☆☆☆</div>
                        <div class="date">2023.12.31</div>
                    </div>
                    <div class="content">집에 보내주세요</div>
                </div>
            </div>
        </div>
    </div>
    <footer></footer>
    <!-- 모달 창 -->
    <div id="reportModal" class="modal">
        <div class="modal-content">
            <h2 class="modal-title">가게 신고</h2>
            <p class="modal-warning">가게 신고 시 돌이킬 수 없으며,<br>허위로 신고를 작성한 경우<br>신고는 무효화 됩니다.</p>
            <label for="reportReason" class="modal-label">신고 사유</label>
            <textarea id="reportReason" class="modal-input"></textarea>
            <button class="modal-submit button">신고하기</button>
        </div>
    </div>
</div>
</body>
</html>
info.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" xmlns="http://www.w3.org/1999/html"
      xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="/css/style.css" rel="stylesheet">
    <link href="/css/user/main.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="innerBox">
    <header></header>
    <div class="container">
        <form action="/menu/info" method="post" class="info-form">
            <img src="/images/sample.png" alt="음식 이미지" class="menu-image">
            <div class="info">
                <div class="title">짜파게티</div>
                <div class="info">맛있는 짜파게티 김치 미포함</div>
                <div class="info">300원</div>
            </div>
            <div class="option">
                <div class="title">
                    <div class="title-text">매운맛</div>
                    <div class="title-option">필수</div>
                </div>
                <div class="contents">
                    <div class="option">
                        <input type="radio" class="d">
                        <p>맵게 해주세요</p>
                        <p>3000원</p>
                    </div>
                    <div class="option">
                        <input type="radio" class="d">
                        <p>맵게 해주세요</p>
                        <p>3000원</p>
                    </div>
                </div>
            </div>
            <div class="option">
                <div class="title">
                    <div class="title-text">토핑</div>
                    <div class="title-option">선택</div>
                </div>
                <div class="contents">
                    <div class="option">
                        <input type="radio" class="d">
                        <p>계란</p>
                        <p>1000원</p>
                    </div>
                    <div class="option">
                        <input type="radio" class="d">
                        <p>치즈</p>
                        <p>500원</p>
                    </div>
                </div>
            </div>
            <div class="count">
                <div class="count-text">수량</div>
                <div class="count-button">
                    <input type="button" class="minus">-</input>
                    <div class="text">1</div>개
                    <input type="button"  class="plus">+</input>
                </div>
            </div>
            <input type="button" class="submit">300원 담기</input>
        </form>
    </div>
    <footer></footer>
</div>
</body>
</html>

 

order.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div class="innerBox">
    <header></header>
    <div class="container">
        <form action="/menu/order" method="post">
            <div class="restaurant-name">식당 이름</div>
            <div class="orders">
                <div class="menu">
                    <div class="text">
                        <div class="title">음식이름</div>
                        <div class="menu-price">가격 : 5,000원</div>
                        <div class="option">옵션 : 안맵게 해주세요(-3000원)</div>
                        <div class="total-price">3000원</div>
                    </div>
                </div>
                <div class="num">
                    <button class="minus">-</button>
                    <div class="text"></div>
                    <button class="plus">+</button>
                </div>
            </div>
            <div class="menuplus">
                +메뉴추가
            </div>
            <div class="request">
                <div class="title">요청사항</div>
                <textarea class="contents" id="contents" name="contents" placeholder="내용을 입력하세요" rows="3">1</textarea>
                <button class="submit">3000원 주문하기</button>
            </div>
        </form>
    </div>
    <footer></footer>
</div>
</body>
</html>

main.css

@charset "UTF-8";

/* 메인 이미지 */
.container .restaurant-image{
    width: 100%;
    height: 200px;
    object-fit: cover;
    object-position: center top;
}
/* 타이틀과 신고버튼 */
.container .title{
    width: 100%;
    padding: 0 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.container .title .restaurant-name{
    font-weight: bold;
}
.container .title .report-button{
    background-color: transparent;
    color: red;
    border: none;
    font-size: 16px;
}
/* 별표와 리뷰 버튼*/
.container .ratings-reviews{
    padding: 0 10px;
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.container .ratings-reviews .ratings{
    display: flex;
}
.container .ratings-reviews .review-button{}

/* 호출 버튼 */
.container .call{
    padding: 0 10px;
    width: 100%;
}
.container .call .call-button {
}

/* 메뉴 공지사항 리뷰 */
.container .buttons{
    border-bottom: 3px solid var(--orange);
    display: flex;
    width: 100%;
}

/* 메뉴 */
.container .buttons .menu-btn{
    border: 0;
    border-radius: 0;
    flex: 1;
}

/* 공지사항 */
.container .buttons .announcement-btn{
    background-color: inherit;
    color: var(--gray);
    border: 0;
    border-radius: 0;
    flex: 1;
}

/* 리뷰 */
.container .buttons .reviews-btn{
    background-color: inherit;
    color: var(--gray);
    border: 0;
    border-radius: 0;
    flex: 1;
}

/* 버튼을 눌렀을때 보여주는 콘텐츠 영역 */
.container .content{
    width: 100%;
}

/* 메뉴 콘텐츠 */
.container .content .menu-content{
    width: 100%;
}

/* 카테고리 슬라이더 부분 */
.container .content .menu-content .category-content{
    margin: auto;
    width: 90%;
}
.container .content .menu-content .category-content .category{
    margin: 10px;
    border-radius: 20px;
    text-align: center;
    background-color: white;
}
.container .content .menu-content .category-content .category:hover{
     transition: 1s;
     background-color: gray;
 }

/* 메뉴 리스트 */
.container .content .menu-content .menus-container {

}
.container .content .menu-content .menus-container .category-name{}
.container .content .menu-content .menus-container .menu{
    width: 100%;
    display: flex;
    border: 1px solid red;
}
.container .content .menu-content .menus-container .menu .image{
    width: 70px;
    height: 70px;
    overflow: hidden;
}
.container .content .menu-content .menus-container .menu .image img{
    width: 70px;
    height: 70px;
}
.container .content .menu-content .menus-container .menu .text{
    display: flex;
    flex-direction: column;
}
.container .content .menu-content .menus-container .menu .text .menu-name{}
.container .content .menu-content .menus-container .menu .text .menu-info{}
.container .content .menu-content .menus-container .menu .text .menu-price{}
/* 공지사항 콘텐츠 */
.container .content .announcement-content{
    width: 100%;
    display: none;
}

.container .content .announcement-content .announcement {
    width: 100%;
    height: 100px;
    display: flex;
    flex-direction: column;
    padding: 10px;
}
.container .content .announcement-content .announcement .title {
    font-size: var(--font-big);
    color: var(--black);

}
.container .content .announcement-content .announcement .content{
    font-size: var(--font-small);
    color: var(--light-gray);
}
.container .content .announcement-content .announcement .date{
    font-size: var(--font-middle);
    color: var(--gray);
}

/* 리뷰 */
.container .content .review-content{
    width: 100%;
    display: none;
}

.container .content .review-content .review{
    margin: 10px;
    padding: 10px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
}
.container .content .review-content .review .title{
    font-size: var(--font-big);
    color: var(--black);
}
.container .content .review-content .review .mid{
    display: flex;
    font-size: var(--font-small);
    color: var(--light-gray);
}
.container .content .review-content .review .mid .star{}
.container .content .review-content .review .mid .date{}
.container .content .review-content .review .content{
    font-size: var(--font-middle);
    color: var(--gray);
}

/* 모달 창 스타일 */
.modal {
    display: none; /* 초기에는 숨김 */
    position: absolute; /* 화면 중앙에 위치 */
    z-index: 1; /* 내용 위에 표시 */
    left: 0;
    top: 0;
    width: 100%; /* 전체 너비 */
    height: 100%; /* 전체 높이 */
    overflow: auto; /* 내용이 넘칠 경우 스크롤 */
    background-color: rgb(0,0,0); /* 검은색 배경 */
    background-color: rgba(0,0,0,0.4); /* 어두운 투명도 */
}

/* 모달 내용 스타일 */
.modal-content {
    background-color: #fefefe;
    margin: 15% auto; /* 페이지 중앙에 위치 */
    padding: 20px;
    border: 1px solid #888;
    width: 300px; /* 너비 설정 */
    height: 300px; /* 높이 설정 */
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

.modal-title {
    text-align: center;
    font-weight: bold;
}

.modal-warning {
    color: red;
    text-align: center;
}

.modal-label {
    text-align: left;
}

.modal-input {
    width: 100%;
    height: 100px;
}

.modal-submit {
    background-color: #222; /* 검은색 배경 */
    color: #fff; /* 하얀색 글씨 */
    text-align: center;
}

main.js

const menu = document.querySelector(".menu-btn");
const announcement = document.querySelector(".announcement-btn");
const reviews = document.querySelector(".reviews-btn");
const menuContent = document.querySelector('.menu-content');
const announcementContent = document.querySelector('.announcement-content');
const reviewContent = document.querySelector('.review-content');

menu.addEventListener('click', () => {
    menu.style.backgroundColor = '#ff7a2f';
    announcement.style.backgroundColor = 'inherit';
    reviews.style.backgroundColor = 'inherit';

    menu.style.color = '#fff';
    announcement.style.color = '#717171';
    reviews.style.color = '#717171';

    menuContent.style.display = 'block';
    announcementContent.style.display = 'none';
    reviewContent.style.display = 'none';
});

announcement.addEventListener('click', () => {
    menu.style.backgroundColor = 'inherit';
    announcement.style.backgroundColor = '#ff7a2f';
    reviews.style.backgroundColor = 'inherit';

    menu.style.color = '#717171';
    announcement.style.color = '#fff';
    reviews.style.color = '#717171';

    menuContent.style.display = 'none';
    announcementContent.style.display = 'block';
    reviewContent.style.display = 'none';
});

reviews.addEventListener('click', () => {
    menu.style.backgroundColor = 'inherit';
    announcement.style.backgroundColor = 'inherit';
    reviews.style.backgroundColor = '#ff7a2f';

    menu.style.color = '#717171';
    announcement.style.color = '#717171';
    reviews.style.color = '#fff';

    menuContent.style.display = 'none';
    announcementContent.style.display = 'none';
    reviewContent.style.display = 'block';
});

// 신고 버튼 클릭 이벤트
document.querySelector('.report-button').addEventListener('click', () => {
    document.getElementById('reportModal').style.display = 'block';
});

// 신고하기 버튼 클릭 이벤트로 모달 창 닫기 (옵션)
document.querySelector('.modal-submit').addEventListener('click', () => {
    document.getElementById('reportModal').style.display = 'none';
});

// 슬릭 슬라이더
$('.category-content').slick({
    dots: false,
    infinite: false,
    speed: 300,
    slidesToShow: 3
});

// 카테고리를 클릭 했을 때 해당 메뉴 부분으로 가능 기능
document.addEventListener('DOMContentLoaded', function() {
    const categories = document.querySelectorAll('.category');

    categories.forEach(category => {
        category.addEventListener('click', function() {
            const categoryId = this.dataset.categoryId;
            scrollToMenuCategory(categoryId);
        });
    });

    function scrollToMenuCategory(categoryId) {
        const menuCategory = document.querySelector(`.menu-category[data-category-id='${categoryId}']`);
        if (menuCategory) {
            menuCategory.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
    }
});

MenuController.java

package kr.ganjuproject.controller;

import kr.ganjuproject.entity.Category;
import kr.ganjuproject.entity.Menu;
import kr.ganjuproject.service.CategoryService;
import kr.ganjuproject.service.MenuService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import java.util.Optional;

@Controller
@Slf4j
@RequestMapping("/menu")
@RequiredArgsConstructor
public class MenuController {

    private final MenuService menuService;
    private final CategoryService categoryService;

    @GetMapping("/main")
    public String main(Model model) {
        List<Category> categories = categoryService.getList();
        model.addAttribute("categories", categories);
        List<Menu> menus = menuService.getList();
        model.addAttribute("menus", menus);
        return "menu/main";
    }

    @GetMapping("/info/{id}")
    public String info(@PathVariable Long id, Model model){
        Optional<Menu> menu = menuService.findById(id);

        if(menu.isPresent()) {
            Menu m = menu.get();
            model.addAttribute("menu", m);
            return"menu/info";
        }else{
            return "redirect:/menu/main";
        }
    }
    @GetMapping("/cart")
    public String cart(){
        return"menu/cart";
    }
    @GetMapping("/order")
    public String order(){
        return"menu/order";
    }
    @GetMapping("/review")
    public String review(){
        return"menu/review";
    }
}

MenuService.java

package kr.ganjuproject.service;

import kr.ganjuproject.entity.Menu;
import kr.ganjuproject.repository.MenuRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MenuService {

    private final MenuRepository menuRepository;

    public List<Menu> getList() {
        return menuRepository.findAll();
    }

    public Optional<Menu> findById(Long id){return menuRepository.findById(id);}
}

MenuRepository.java

package kr.ganjuproject.repository;

import kr.ganjuproject.entity.Menu;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MenuRepository extends JpaRepository<Menu, Long> {
}

CategoryService.java

package kr.ganjuproject.service;

import kr.ganjuproject.entity.Category;
import kr.ganjuproject.repository.CategoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CategoryService {
    private final CategoryRepository categoryRepository;

    public List<Category> getList(){
        return categoryRepository.findAll();
    }
}

어제는 결재 테스트 한다고 아무것도 못한 느낌이 었는데 오늘은 할만했

'공부 > Ganju' 카테고리의 다른 글

[Spring/AWS] 팀프로젝트 7일차  (0) 2024.04.04
[Spring/AWS] 팀프로젝트 6일차  (0) 2024.04.03
[Spring/AWS] 팀프로젝트 4일차  (0) 2024.04.02
[Spring/AWS] 팀프로젝트 3일차  (0) 2024.03.29
[Spring/AWS] 팀프로젝트 2일차  (0) 2024.03.28

오늘은 인원의 부재로 프로젝트 시작 전 공부 및

 

테스트 프로젝트 생성 연습

 

참고 강의 사이트

 

https://www.udemy.com/course/spring-boot-and-spring-framework-korean/#reviews

 

 

테스트를 한 것

 

1. 3 tries layer [controller - service - repository ] 를 이용한 회원 가입 게시판 페이지

 

2. Spring Boot + Gradle + MySQL + JPA + Thymeleaf  를 이용한 회원가입 게시판

 

3. 구글 네이버 로그인 테스트

 

4. aws 배포시 동시성 테스트

 

 

'공부 > Ganju' 카테고리의 다른 글

[Spring/AWS] 팀프로젝트 6일차  (0) 2024.04.03
[Spring/AWS] 팀프로젝트 5일차  (0) 2024.04.02
[Spring/AWS] 팀프로젝트 4일차  (0) 2024.04.02
[Spring/AWS] 팀프로젝트 2일차  (0) 2024.03.28
[Spring/AWS] 팀프로젝트 1일차  (1) 2024.03.27

테이블 정의서와 업무 기술서 작성, ERD 다이어그램 및 아이디어 회의

 

원본 링크

 

https://docs.google.com/spreadsheets/d/1vpkdBtokRiqw9oKwmQFbQH-7XgaGfExxW57HTRjsSi0/edit#gid=0

 

팀 프로젝트-테이블오더

요구사항 정의서 [요구사항 정의서] 구분,요구사항 ID,서비스 (메뉴),필요기능,기능설명,변경구분,변경일자,변경 내용,비고 공통,REQ-001,신규 REQ-002,신규 REQ-003,신규 REQ-004,신규 REQ-005,신규 REQ-006,신

docs.google.com

 

업무 기술서

업무기술서
시스템명 모바일 키오스크(웹/앱) 단위 업무명 관리자
1) 관리자는 고유 넘버와 아이디, 비밀번호를 가지고 있다.
2) 관리자는 식당을 승인, 거부할 수 있다.
3) 관리자는 게시판의 추가, 수정, 삭제 권한을 가지고 있다.
4) 관리자는 식당의 리스트를 확인하고, 관리할 수 있다.
5) 관리자는 식당별 손님의 신고를 확인할 수 있다.
 
업무기술서
시스템명 모바일 키오스크(웹/앱) 단위 업무명 식당 관리
1) 식당은 식당 번호, 이름, 주소, 전화번호, 테이블 수, 메인 이미지, 로고, 메뉴, 리뷰, 공지사항을 가지고 있다.
2) 첫 가입 시 시스템 사용을 위해서 관리자의 승인을 받아야 한다.
3) 하나의 식당은 여러 메뉴를 가질 수 있다.
4) 식당은 리뷰를 삭제하거나, 공지사항을 작성할 수 있다.
5) 식당은 메뉴를 관리하거나, 카테고리를 추가할 수 있다.
6) 식당은 고객의 주문을 취소, 승인할 수 있다.
7) 식당은 날짜별 매출을 확인할 수 있다.
 
업무기술서
시스템명 모바일 키오스크(웹/앱) 단위 업무명 메뉴 관리
1) 메뉴는 메뉴 번호, 카테고리, 이름, 가격, 메뉴 이미지, 옵션, 상세설명을 가지고 있다.
2) 메뉴는 카테고리를 선택적으로 가질 수 있다.
3) 메뉴는 등록, 수정 시 쿠폰 및 이벤트 할인을 적용할 수 있다.
4) 상세설명은 메뉴의 구성, 원산지, 설명 등을 나타낸다.
5) 메뉴는 여러가지의 추가/선택 옵션을 가질 수 있다.
6) 옵션은 옵션 번호, 이름, 속성값을 가지고 있으며, 필수와 선택으로 나뉜다.
 
업무기술서
시스템명 모바일 키오스크(웹/앱) 단위 업무명 손님 관리
1) 손님은 테이블 번호로 구분된다.
2) 손님은 주문을 장바구니에 넣을 수 있다.
3) 손님은 주문 시 요구사항을 작성할 수 있다.
4) 주문 결제 시 더치페이와 모두 결제를 선택할 수 있다.
4) 주문을 완료한 손님은 리뷰를 작성할 수 있다.
5) 손님은 직원을 호출하거나, 물 혹은 티슈등을 요청할 수 있다.
6) 손님은 식당을 신고할 수 있으며, 사유를 적어야 한다.
 
업무기술서
시스템명 모바일 키오스크(웹/앱) 단위 업무명 주문 관리
1) 주문은 테이블 번호, 메뉴 이름, 수량, 금액, 주문 시간, 요구사항을 가지고 있다.
2) 금액은 메뉴별 금액과 총 금액으로 나뉜다.
3) 주문 시간은 손님이 결제 완료한 시간으로 나타난다.

 

테이블 정의서

테이블명 컬럼명 데이터 타입(길이) PK FK 참조테이블 NULL 허용 비고
manager
매니저
고유넘버 id LONG Y     NOT NULL auto_increment
아이디 login_id VARCHAR(30)       NOT NULL 영문(필수), 숫자(가능) 포함 3자이상 20자 미만
비밀번호 password VARCHAR(30)       NOT NULL 영문, 숫자(필수) 포함 4자이상 20자 미만
이메일 email VARCHAR(255)       NOT NULL 이메일 양식으로(ex - test1@test.com)
핸드폰 phone VARCHAR(30)       NOT NULL 번호 양식(010-1234-1234)
식당 고유넘버 restaurant_id LONG   Y restaurant    
restaurant
식당
고유넘버 id LONG Y     NOT NULL auto_increment
식당 이름 name VARCHAR(30)       NOT NULL 한글, 영어, 숫자, 특수문자(,.!?())가능
식당 주소 address VARCHAR(255)       NOT NULL  
식당 전화번호 phone VARCHAR(30)       NOT NULL 번호 양식(010-1234-1234)
테이블 수 restaurant_table INT       NOT NULL 1이상
로고 logo VARCHAR(255)       NOT NULL .jpeg, jpg, .png 허용
restaurant_image
식당 이미지
식당 고유넘버 restaurant_id LONG   Y restaurant NOT NULL  
이름 name VARCHAR(255)       NOT NULL .jpeg, jpg, .png 허용
category
카테고리
고유넘버 id LONG Y     NOT NULL auto_increment
이름 name VARCHAR(30)       NOT NULL  
menu
메뉴
고유넘버 id LONG Y     NOT NULL auto_increment
카테고리 category_id LONG   Y category    
이름 name VARCHAR(30)       NOT NULL 한글, 영어, 숫자, 특수문자(,.!?())가능
가격 price INT       NOT NULL 1이상
사진 menu_image VARCHAR(255)         .jpeg, jpg, .png 허용
상세설명 info VARCHAR(1000)          
menu_option
옵션
고유넘버 id LONG Y     NOT NULL auto_increment
이름 name VARCHAR(30)       NOT NULL 한글, 영어, 숫자, 특수문자(,.!?())가능
필수/선택 kind VARCHAR(30)       NOT NULL Required / Optional
메뉴 고유넘버 menu_id LONG   Y menu NOT NULL  
menu_option_value
옵션 속성값
고유넘버 id LONG Y     NOT NULL auto_increment
내용 content VARCHAR(1000)       NOT NULL  
옵션 고유넘버 menu_option_id LONG   Y menu_option NOT NULL  
review
리뷰
고유넘버 id LONG Y     NOT NULL auto_increment
식당 고유넘버 restaurant_id LONG   Y restaurant NOT NULL  
유저의 닉네임 name VARCHAR(30)       NOT NULL 한글, 영어, 숫자, 특수문자(,.!?())가능
내용 content VARCHAR(1000)       NOT NULL  
작성 시간 reg_date DateTime       NOT NULL  
별점 star INT       NOT NULL 0 - 10
주문 고유넘버 orders_id LONG   Y orders    
공개유무 secret INT       NOT NULL 0 / 1
orders
주문
고유넘버 id LONG Y     NOT NULL auto_increment
테이블 번호 restaurant_table_no INT       NOT NULL  
메뉴 번호 menu_id LONG   Y menu NOT NULL  
금액 price INT       NOT NULL  
시간 reg_date DATETIME       NOT NULL  
요구사항 content VARCHAR(1000)          
board
게시판
고유넘버 id LONG Y     NOT NULL auto_increment
작성자 name VARCHAR(30)       NOT NULL  
제목 title VARCHAR(1000)       NOT NULL  
내용 content VARCHAR(1000)          
날짜 reg_date DATETIME       NOT NULL  
카테고리 board_category VARCHAR(30)       NOT NULL NOTICE, REPORT, QUESTION

 

 

ERD 다이어그램 ( 윤경재 )

 

'공부 > Ganju' 카테고리의 다른 글

[Spring/AWS] 팀프로젝트 6일차  (0) 2024.04.03
[Spring/AWS] 팀프로젝트 5일차  (0) 2024.04.02
[Spring/AWS] 팀프로젝트 4일차  (0) 2024.04.02
[Spring/AWS] 팀프로젝트 3일차  (0) 2024.03.29
[Spring/AWS] 팀프로젝트 1일차  (1) 2024.03.27

+ Recent posts