MVC 기반 점심 메뉴 추천기

2025. 9. 10. 12:18·JAVA/Java 실습

⛳️ 과제 목표

  • Servlet, JSP, Java 클래스(Service/Model)로 역할을 분리한 MVC 패턴 학습
  • HTTP 요청을 처리하는 구조와 데이터 전달 흐름 이해
  • 데이터를 List<Menu> 형태로 관리하고 JSP에서 출력하기

📚 필수 과제

[프로젝트 구조]

practical4
ㄴ src
	ㄴ MenuController
	ㄴ Menu
	ㄴ MenuService
ㄴ web
	ㄴ WEB-INF
		ㄴ web.xml
	ㄴ form.jsp
	ㄴ index.jsp
	ㄴ list.jsp
	ㄴ select.jsp

 

1. 점심 메뉴 등록 폼 페이지 구성

더보기
더보기
  • form.jsp
  • 사용자로부터 메뉴 이름, 맵기, 가격 등의 정보를 입력받습니다. 
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>점심메뉴 등록</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container { max-width:900px; margin:40px auto; padding:24px;
            background: rgba(118, 196, 106, 0.74); border-radius:16px;
            box-shadow:0 10px 24px rgba(0,0,0,.06); text-align:center;}
        .btn-submit { margin: 10px 0 auto auto; padding: 12px 18px; border:0; border-radius:5px; cursor:pointer;
            align-items: center;}
        .form-wrap { text-align:left; margin-top:18px; }
        .form-wrap .form-label { font-weight:600; }
        .actions { display:flex; gap:8px; margin-top:14px; }
    </style>
</head>
<body>
    <div class="container" style="width: 500px;">
    <h2>🍱 점심 메뉴 등록 화면</h2>
        <form action="${pageContext.request.contextPath}/menu" method="post">
            <div class="form-wrap">
              <div class="mb-3">
                <label class="form-label" for="name">음식명</label>
                <input id="name" name="name" class="form-control" required>
              </div>

              <div class="mb-3">
                <label class="form-label" for="spice">맵기</label>
                <select id="spice" name="spice" class="form-select">
                  <option value="MILD">순함</option>
                  <option value="NORMAL">보통</option>
                  <option value="HOT">매움</option>
                </select>
              </div>

              <div class="mb-3">
                <label class="form-label" for="price">가격</label>
                <input id="price" name="price" class="form-control" type="number" min="0" required>
              </div>

              <button type="submit" class="btn btn-primary w-100">등록</button>

              <div class="actions">
                <a class="btn btn-outline-light flex-fill" href="${pageContext.request.contextPath}/menu?action=list">메뉴 목록</a>
                <a class="btn btn-outline-dark flex-fill" href="${pageContext.request.contextPath}/menu?action=select">랜덤 추천</a>
              </div>
            </div>
        </form>
</div>
</body>
</html>

 

스타일시트는 BootStrap 기반의 css 기능을 활용하였고, 상세한 css 세팅까지는 오랜 시간이 드는 관계로 ChapGPT의 도움을 적극적으로 받았음

 

form 형태의 입력값을 받고 입력받은 값을 Post 할 수 있게 기능을 설정하였음

 

맵기의 단계는 문제의 요건에 따라 순함, 보통, 매움으로 구분하여 진행하였음

2. 메뉴 등록 요청 처리 

더보기
더보기
  • MenuController로 입력값을 받아 MenuService로 전달하고 메뉴 리스트를 추가합니다.
  • MenuService에는 static한 menuList가 있습니다.
  • static ?
    • static은 공용이라는 뜻이에요.
    • 클래스가 만들어질 때 딱 하나만 만들어지고, 모든 사람이 같이 쓰는 것이라고 생각하면 돼요.
  • final?
    • 변하지 않는 값이에요.
//MenuController.java

package practical4;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/menu")
public class MenuController extends HttpServlet {
    private MenuService service = new MenuService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String action = request.getParameter("action");
        System.out.println("[GET] /menu action=" + action);

        // 1) 등록 폼
        if ("form".equals(action)) {
            System.out.println("[ROUTE] forward -> /form.jsp");
            request.getRequestDispatcher("/form.jsp").forward(request, response);
            return;
        }

        // 2) 조건 선택 화면
        if ("select".equals(action)) {
            System.out.println("[ROUTE] forward -> /select.jsp");
            request.getRequestDispatcher("/select.jsp").forward(request, response);
            return;
        }

        // 3) 기본: 목록 화면
        request.setAttribute("menus", service.getMenuList());
        System.out.println("[ROUTE] forward -> /list.jsp (size=" + (service.getMenuList() == null ? 0 : service.getMenuList().size()) + ")");
        request.getRequestDispatcher("/list.jsp").forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String action = request.getParameter("action");

        // 조건 추천 처리: POST /menu?action=recommend
        if ("recommend".equals(action)) {
            String basis = request.getParameter("basis");   // price | spice
            String detail = request.getParameter("detail"); // P0_10.. | MILD/NORMAL/HOT

            java.util.List<Menu> all = service.getMenuList();
            java.util.List<Menu> candidates = new java.util.ArrayList<Menu>();
            if ("price".equals(basis)) {
                int min = 0, max = Integer.MAX_VALUE;
                if ("P0_10".equals(detail)) { min = 0; max = 10000; }
                else if ("P10_30".equals(detail)) { min = 10000; max = 30000; }
                else if ("P30_50".equals(detail)) { min = 30000; max = 50000; }
                else if ("P50_UP".equals(detail)) { min = 50000; max = Integer.MAX_VALUE; }
                for (Menu m : all) if (m.getPrice() >= min && m.getPrice() < max) candidates.add(m);
            } else if ("spice".equals(basis)) {
                for (Menu m : all) if (detail != null && detail.equals(m.getSpice())) candidates.add(m);
            }

            Menu picked = null;
            if (!candidates.isEmpty()) {
                picked = candidates.get(new java.util.Random().nextInt(candidates.size()));
            }

            if (picked != null) request.setAttribute("menu", picked);
            else request.setAttribute("emptyMsg", "조건에 맞는 메뉴가 없어요. 먼저 등록해 주세요!");

            request.getRequestDispatcher("/select.jsp").forward(request, response);
            return;
        }

        // 기본: 등록 처리 (POST /menu)
        String name  = request.getParameter("name");
        String spice = request.getParameter("spice");
        int price    = Integer.parseInt(request.getParameter("price"));

        service.addMenu(new Menu(name, spice, price));
        response.sendRedirect(request.getContextPath() + "/menu?action=list");
    }
}

 

기본 WebServlet 개념과 기능을 적용하여 POST / GET 형태로 요청을 받고 요청받은 값을 그대로 보여주는 형태로 구성하였음

 

3. 메뉴 목록 조회 페이지

더보기
더보기
  • List<Menu> 데이터를 JSP에서 반복문으로 출력합니다.
  • 등록된 메뉴들의 이름, 맵기, 가격을 테이블 형태로 출력됩니다.

 

//list.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.List, practical4.Menu" %>
<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>점심메뉴 목록</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container py-4">
  <h2 class="mb-4">🍱 점심메뉴 목록</h2>

  <%
    // 컨트롤러에서 넘겨준 속성들
    List<Menu> menus = (List<Menu>) request.getAttribute("menus");
  %>

  <!-- 메뉴 테이블 -->
  <% if (menus == null || menus.isEmpty()) { %>
  <div class="card mb-3 text-center p-4">
      <div class="mb-2">😢 등록된 점심 메뉴가 아직 없어요!</div>
  </div>
  <% } else { %>
    <div class="table-responsive">
      <table class="table table-striped align-middle">
        <thead class="table-light">
          <tr>
            <th style="width:80px;">#</th>
            <th>이름</th>
            <th style="width:140px;">맵기</th>
            <th style="width:160px;">가격</th>
          </tr>
        </thead>
        <tbody>
        <% for (int i = 0; i < menus.size(); i++) { Menu m = menus.get(i); %>
          <tr>
            <td><%= i + 1 %></td>
            <td><%= m.getName() %></td>
            <td><%= m.getSpice() %></td>
            <td><%= m.getPrice() %>원</td>
          </tr>
        <% } %>
        </tbody>
      </table>
    </div>
  <% } %>

  <div class="mt-4 d-flex gap-2">
    <a class="btn btn-primary" href="<%= request.getContextPath() %>/menu?action=form">메뉴 등록</a>
    <a class="btn btn-success" href="<%= request.getContextPath() %>/menu?action=select">조건 기반 추천</a>
  </div>
</body>
</html>

 

 


❤️‍🔥 도전 과제

1. 랜덤 추천 기능

더보기
더보기
  • 점심 메뉴를 고르기 어려운 사용자를 위한 자동 추천 기능 제공합니다.
  • select.jsp를 추가하고, 추천을 위한 선택 조건을 선택하는 화면을 만듭니다.
  • <select>를 두개 만듭니다.
    • 첫번째 <select>에는 총 2개의 option이 있습니다. (가격, 맵기)
    • 두번째 <select>는 첫번째 select 값에 따라 동적으로 변경됩니다.
      • 가격 선택시 0~10,000 | 10,000~30,000 | 30,000~50,000 | 50,000~ ****총 4개의 option이 뜹니다.
      • 맵기 선택시 매움, 보통, 순함 총 3개의 option이 뜹니다.

 

//select.jsp

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="practical4.Menu" %>
<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>조건 기반 추천</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container py-4">
  <h2 class="mb-4">조건 기반 추천</h2>

  <%
    Menu menu = (Menu) request.getAttribute("menu");
    String emptyMsg = (String) request.getAttribute("emptyMsg");
  %>

  <% if (menu != null) { %>
    <div class="card mb-3 shadow-sm">
      <div class="card-body d-flex justify-content-between align-items-center">
        <div>
          <h5 class="card-title mb-1">오늘의 추천</h5>
          <div class="card-text">
            <strong><%= menu.getName() %></strong>
            <span class="ms-2 text-muted">(맵기: <%= menu.getSpice() %> / 가격: <%= menu.getPrice() %>원)</span>
          </div>
        </div>
        <a class="btn btn-outline-primary btn-sm" href="<%= request.getContextPath() %>/menu?action=select">다시 선택</a>
      </div>
    </div>
  <% } else if (emptyMsg != null) { %>
    <div class="card mb-3 text-center p-4">
      <div class="mb-2">😢 <%= emptyMsg %></div>
      <a class="btn btn-success" href="<%= request.getContextPath() %>/menu?action=form">메뉴 등록하러 가기</a>
    </div>
  <% } %>

  <form action="<%= request.getContextPath() %>/menu?action=recommend" method="post" class="row g-3">
    <div class="col-12 col-md-4">
      <label class="form-label" for="basis">기준</label>
      <select id="basis" name="basis" class="form-select" required>
        <option value="" disabled selected>-- 선택 --</option>
        <option value="price">가격</option>
        <option value="spice">맵기</option>
      </select>
    </div>

    <div class="col-12 col-md-4">
      <label class="form-label" for="detail">세부</label>
      <select id="detail" name="detail" class="form-select" required disabled>
        <option value="" disabled selected>먼저 기준을 선택하세요</option>
      </select>
    </div>

    <div class="col-12 col-md-4 align-self-end">
      <button type="submit" class="btn btn-primary w-100">추천 받기</button>
    </div>
  </form>

  <div class="mt-4 d-flex gap-2">
    <a class="btn btn-outline-secondary" href="<%= request.getContextPath() %>/menu?action=list">목록</a>
    <a class="btn btn-outline-dark" href="<%= request.getContextPath() %>/menu?action=form">등록</a>
  </div>

  <script>
  document.addEventListener('DOMContentLoaded', () => {
    const basis = document.getElementById('basis');
    const detail = document.getElementById('detail');
    const options = {
      price: [
        {v:'P0_10',  t:'0~10,000원'},
        {v:'P10_30', t:'10,000~30,000원'},
        {v:'P30_50', t:'30,000~50,000원'},
        {v:'P50_UP', t:'50,000원 이상'}
      ],
      spice: [
        {v:'MILD',   t:'순함'},
        {v:'NORMAL', t:'보통'},
        {v:'HOT',    t:'매움'}
      ]
    };
    basis.addEventListener('change', () => {
      const list = options[basis.value] || [];
      detail.innerHTML = '';
      if (list.length === 0) {
        detail.disabled = true;
        const opt = document.createElement('option');
        opt.value=''; opt.textContent='해당 옵션이 없습니다';
        opt.disabled = true; opt.selected = true;
        detail.appendChild(opt);
        return;
      }
      list.forEach(o => {
        const opt = document.createElement('option');
        opt.value = o.v; opt.textContent = o.t;
        detail.appendChild(opt);
      });
      detail.disabled = false;
    });
  });
  </script>
</body>
</html>

 

2. 추천 결과를 카드 형태로 스타일링

더보기
더보기
  • 추천 메뉴는 Bootstrap 카드 스타일로 이쁘게 표시 (이름, 설명, 감정 이모지 등 포함) 합니다.

 

  //list.jsp 에서 사용
  
  <% if (menus == null || menus.isEmpty()) { %>
  <div class="card mb-3 text-center p-4">
      <div class="mb-2">😢 등록된 점심 메뉴가 아직 없어요!</div>
  </div>
  
  //select.jsp 에서 사용
  
  else if (emptyMsg != null) { %>
    <div class="card mb-3 text-center p-4">
      <div class="mb-2">😢 <%= emptyMsg %></div>
      <a class="btn btn-success" href="<%= request.getContextPath() %>/menu?action=form">메뉴 등록하러 가기</a>
    </div>
  <% } %>

 

3. 등록된 메뉴가 없을 때 안내 페이지로 연결 시키기

더보기
더보기
  • 등록된 메뉴가 없을 때 random.jsp나 list.jsp를 접근했을 때 새로 띄워줄 화면을 만들어보세요.
  • 예시 : ‘등록된 메뉴가 없어요! 등록을 먼저 진행해주세요!’ 문구 띄우는 안내 페이지로 연결되도록 처리

 

random.jsp 는 따로 진행하지 않음 (select.jsp에서 조건 기반 랜덤하게 출력을 해줌) 

 

위의 기재한 방식과 같게 값이 공백이거나 null 값일 때 메세지를 노출시키도록 진행하였음


☠️ 트러블 슈팅

  • 발생한 Issue 
    • ① 전반적인 jsp 화면 구성이 원활하게 나타나지 않는 상황
      • 폼 제출 이후 화면이 하얗게 나오거나 비어있게 보이는 상황
    • ② form 화면을 놓고 등록버튼을 눌렀을 때 list 가 저장되지 않는 상황
    • ③ list가 저장되고 난 이후에 조건 기반 추천을 누르면 추천을 하지 않고 Empty인 경우만 보여주는 상황
    • ④ 수정과정 중 어느 단계에서의 문제점인지 몰라 전반적인 재작성 중 코드가 완전히 꼬여버린 상황
    • ⑤ 구성하고자 하는 파일의 구조와 각 파일의 역할에 대해 정확하게 이해하지 못하고 문제를 풀다보니 코드 구성 자체가 꼬이거나 어디를 손대야 할지 모르겠었음 
  • 해결방법 / 찾아본 내용
    • ① JSP로 직접 링크를 걸게 되어 Controller를 통해 처리되지 않아 발생한 이슈
      • 컨트롤러 경유 URL 을 사용하여 request.setAttribute("menus")를 넣어 이동할 수 있게 해야함
    • ②,③ value / 라벨 불일치로 인하여 조건에 매칭되지 않아 계속 Empty인 상황일 때 
    • ④ 문제가 발생한 프로젝트는 그대로 유지하고 우선 새로운 프로젝트로 문제를 다시 접근하고 해결해보고자 함 (문제를 찾는 것 또한 중요)
      • 현재 문제점은 목록까지는 저장을 잘 하나 조건 기반 추천이 아예 진행이 안되는 상태
      • 주요 문제점은 Controller를 통해 버튼 작동 및 페이지의 연결이 되도록 올바른 명령어를 써야한다는 것
      • 진행 도중 디버그 기능을 적극 활용하여 어느 수행단계에서 오류가 발생하는지에 대해서 확실하게 알 것
        • IntelliJ 콘솔창에서도 친절하게 위치를 알려주긴 하나, 어떤 식으로 바꿔야 하는지에 대해서도 열심히 방법을 찾아봐야 함
    • ⑤ 구성하고자 하는 파일의 구조와 각 파일의 역할에 대해 먼저 정리하고 DTO, Controller 등 어떤 기능들을 먼저 담아야 하는지 정리하고 코드 작성에 들어갈 것 !! 

 

'JAVA > Java 실습' 카테고리의 다른 글

[JAVA] 계산기 과제 개념 추가 정리  (0) 2025.09.16
점메추 SPA 에디션  (0) 2025.09.10
JSP 자기소개 버전 만들기  (0) 2025.09.09
나를 소개합니다  (0) 2025.09.08
JSP / Servlet 실습  (0) 2025.09.05
'JAVA/Java 실습' 카테고리의 다른 글
  • [JAVA] 계산기 과제 개념 추가 정리
  • 점메추 SPA 에디션
  • JSP 자기소개 버전 만들기
  • 나를 소개합니다
stark77
stark77
하마의 IT 자기개발 이모저모, 백엔드 개발자로 거듭나기
  • stark77
    하마의 개발자 성장일기
    stark77
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 컴퓨터구조와 운영체제
        • 컴퓨터구조
        • 운영체제
      • SQL 기초
      • Spring
        • 백엔드 기초
        • Spring 실습
      • JAVA
        • Java 실습
      • HTML&CSS
        • HTML&CSS 실습
      • Git&GitHub
        • Git&GitHub 실습
      • 내배캠 끄적끄적
        • Today I Learned
      • 유용한 툴 및 사이트 정리
      • 취미
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Til
    WebSocket
    다형성
    String.format
    객체지향프로그래밍
    Spring
    경합조건과 교착상태
    웹소켓
    Spriingboot
    thymleaf
    BEAN
    백엔드 기초
    객체지향
    for문
    HTML&CSS
    java
    JPA
    MVC
    네트워크 기초
    프로세스와 쓰레드
    SpringSecurity
    git
    실시간 데이터 처리
    백엔드 기초다지기
    Stomp
    RestTemplate
    jsp
    Github
    Java 문법기초
    algorithm
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
stark77
MVC 기반 점심 메뉴 추천기
상단으로

티스토리툴바