IT 프로젝트/쇼핑몰 만들기

[Spring Boot] 스프링 부트 프로젝트/쇼핑몰 만들기 - 메뉴

happygram 2020. 1. 18. 18:57

설명

데이터베이스와 연동하여 메뉴 구성을 진행 해보겠습니다.
스키마 생성 후 초기 데이터를 미리 생성 해두고 어플리케이션에서 데이터를 이용하여 메뉴 구성을 진행 합니다.

데이터베이스

스키마

이전 글을 참조해서 생성을 진행합니다.

데이터베이스 모델 설계

데이터

초기 데이터를 아래의 SQL로 생성 합니다.

-- 의류
INSERT INTO category
(id, id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(1, 0, '의류', 'fas fa-tshirt', '의류', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(1, '아우터', 'far fa-circle', '아우터', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(1, '티셔츠', 'far fa-circle', '티셔츠', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(1, '바지', 'far fa-circle', '바지', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

-- 신발
INSERT INTO category
(id, id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(2, 0, '신발', 'fas fa-shoe-prints', '신발', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(2, '운동화', 'far fa-circle', '운동화', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(2, '로퍼', 'far fa-circle', '로퍼', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(2, '부츠', 'far fa-circle', '부츠', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

-- 악세사리
INSERT INTO category
(id, id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(3, 0, '악세사리', 'fas fa-hat-wizard', '악세사리', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(3, '양말', 'far fa-circle', '양말', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(3, '모자', 'far fa-circle', '모자', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO category
(id_parent, title, icon, description, create_timestamp, update_timestamp)
VALUES(3, '가방', 'far fa-circle', '가방', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

트리 구조로 생성되도록 합니다.
parent 가 되는 레코드는 id 값을 1~100 값을 갖도록 하고, id_parent 값은 0 값을 설정하여 부모 노드를 갖지 않도록 합니다.
child 가 되는 레코드는 id 값이 101 값부터 Auto Increment 하도록 하고, id_parent 값은 각각 카테고리 영역에 맞도록 부모 id 값을 지정합니다.
(최상위 메뉴는 100개까지 존재한다고 가정 하였습니다. 100개가 초과하는 경우 그 이상 갖도록 설정하셔도 좋습니다)

다음의 모습의 구조를 기대 합니다.

의류
 ├─아우터
 ├─티셔츠
 └─바지
신발
 ├─운동화
 ├─로퍼
 └─부츠
악세사리
 ├─양말
 ├─모자
 └─가방

Back-End

Category.java

package happygram.ecommerce.jpa.domain;

import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Getter
@Entity
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long idParent;

    private String title;

    private String icon;

    private String description;

    @CreationTimestamp
    private LocalDateTime createTimestamp;

    @UpdateTimestamp
    private LocalDateTime updateTimestamp;

    @Builder
    public Category(Long idParent, String title, String icon, String description) {
        this.idParent = idParent;
        this.title = title;
        this.icon = icon;
        this.description = description;
    }
}

데이터베이스 모델에 맞게 Entity 클래스를 작성합니다.

CategoryRepository.java

package happygram.ecommerce.jpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import happygram.ecommerce.jpa.domain.Category;

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long>{
}

JPA API 사용하여 기본적인 Repository 클래스를 작성합니다.

CategoryService.java

package happygram.ecommerce.service;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import happygram.ecommerce.constant.CategoryConstant;
import happygram.ecommerce.jpa.domain.Category;
import happygram.ecommerce.jpa.repository.CategoryRepository;

@Service
public class CategoryService {

    @Autowired
    private CategoryRepository categoryRepository;

    public Map<Category, List<Category>> getCategory() {
        List<Category> menuList = categoryRepository.findAll();

        // Parent
        List<Category> parentMenuList = menuList.stream()
            .filter(category -> category.getIdParent() == CategoryConstant.PARENT_ROOT)
            .collect(Collectors.toList())
            ;

        // Child
        Map<Long, List<Category>> childMenuMap = menuList.stream()
            .filter(category -> category.getIdParent() != CategoryConstant.PARENT_ROOT)
            .collect(Collectors.groupingBy(Category::getIdParent))
            ;

        // TreeMenu
        Map<Category, List<Category>> menuMap = new HashMap<>();
        for(Category parent : parentMenuList){
            Long id = parent.getId();
            List<Category> child = childMenuMap.get(id);
            menuMap.put(parent, child);
        }
        Map<Category, List<Category>> sortedMenuMap = 
            menuMap.entrySet().stream()
            .sorted((e1, e2)-> e1.getKey().getId().compareTo(e2.getKey().getId()))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));

        return sortedMenuMap;
    }
}

Repository 에서 데이터를 가져와서 트리 구조로 만드는 로직을 구현 하였습니다.
현재는 2레벨 까지만 존재한다는 가정에서 로직을 구현 하였습니다. 썩 마음에 드는 로직은 아니지만, 현재는 이것이 최선이었네요.. ㅎㅎ
사실 로직 자체도 100% 마음에 들지는 않아서 나중에 해당 메소드는 다시 깔끔하게 구현할 예정입니다.

GlobalControllerAdvice.java

package happygram.ecommerce.config;

import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

import happygram.ecommerce.jpa.domain.Category;
import happygram.ecommerce.service.CategoryService;

@ControllerAdvice
public class GlobalControllerAdvice {

    @Autowired
    private CategoryService categoryService;

    @ModelAttribute
    public void handleRequest(HttpServletRequest request, Locale locale, Model model) {
        String servletPath = request.getServletPath();

        // view 페이지 호출할 때 메뉴 조립
        if(!servletPath.isEmpty() && 
        (
            servletPath.equals("/") || servletPath.contains("/view/")
            )
        ){
            Map<Category, List<Category>> categoryList = categoryService.getCategory();
            System.out.println(categoryList);
            model.addAttribute("categories", categoryList);
        }
    }
}

모든 Rest API 호출 시 메뉴를 완성해서 제공하지 않을 것이기 때문에, 루트 패스 및 'view' 경로를 갖고 있는 경우에만 CategoryService 를 호출하도록 하였습니다.

Front-End

sidebar.html

<aside class="main-sidebar sidebar-dark-primary elevation-4">
  <!-- Brand Logo -->
  <a th:href="@{/}" class="brand-link text-center">
    <span class="brand-text font-weight-light">Happygram</span>
  </a>

  <!-- Sidebar -->
  <div class="sidebar">

    <!-- Sidebar Menu -->
    <nav class="mt-2">
      <ul class="nav nav-pills nav-sidebar flex-column nav-child-indent" data-widget="treeview" role="menu" data-accordion="false">
        <!-- Add icons to the links using the .nav-icon class
            with font-awesome or any other icon font library -->

        <li class="nav-item has-treeview" th:each="category : ${categories}">
          <a href="#" class="nav-link">
            <i th:class="nav-icon" th:classappend="${category.key.icon}"></i>
            <p> 
              <span th:text="${category.key.title}" th:remove="tag"></span>
              <i class="right fas fa-angle-left"></i>
            </p>
          </a>
          <ul class="nav nav-treeview" th:each="child : ${category.value}">
            <li class="nav-item">
              <a href="@{/product/view/list}" class="nav-link">
                <i class="nav-icon" th:classappend="${child.icon}"></i>
                <p th:text="${child.title}"></p>
              </a>
            </li>
          </ul>
        </li>
      </ul>
    </nav>
    <!-- /.sidebar-menu -->
  </div>
  <!-- /.sidebar -->
</aside>

ControllerAdvice 에서 전달받은 categories Key 를 반복문으로 처리하면서 메뉴를 완성하도록 하였습니다.