[카페24] 카페24 카테고리 코드 분석

2022. 8. 7. 15:31StartUP/카페24

728x90

 

 

카페24 기능 중 가장 어려웠던 부분이 바로 이 '카테고리' 부분이다.

다른 모듈들은 div나 li를 두 번 반복해서 넣으면 자동으로 반복문이 되거나 변수명이 넣으면 자동으로 통신해서 받아오는 등  스크립트 기능들이 숨겨져 있는데(호스팅을 이용하는 사용자에게 노출되지 않는다는 뜻)

이 카테고리 노출하는 모듈이 거의 유일무이하게 category.js로 바깥 노출이 되어 있다.

내부를 살펴보면 카페24에서 front API라고 부르는 exec/front 주소로 ajax 통신을 진행하고

해당 주소로 들어가보면 쇼핑몰관리자 - 상품 - 상품 분류에서 작업한 카테고리가 depth 없는 JSON으로 노출된다.

(카페24를 사용하는 모든 쇼핑몰은 도메인 + /exec/front/Product/SubCategory 를 주소창에 넣으면 카테고리 JSON이 나온다)

 

여기서 문제는 depth가 없는 JSON이라 대-중-소-상세 까지 있는 카페24 카테고리를 직접 객체로 만들어야 커스텀 할 수 있는데,

이번에 우리 카테고리 페이지를 갈아 엎으면서 이 문제의 category.js에 주석으로 어떤 기능을 하는지 적어보았다.

그리고 분석한 내용을 기반으로 별도로 fetch 문으로 작동하는 새로운 로직으로 적용한 페이지를 개발했다.

ajax 통신 이후에 for문으로 반복하는 것이 depth 없는 json을 정리해서 카테고리 번호 : 카테고리 배열 로 만드는 부분이라 이 부분만 이해해도 커스텀하기에는 어렵지 않다.

나머지는 부모 카테고리에 마우스 오버 했을 때 표시하는 HTML을 만드는 함수다.

 

 

 

/**
 * 카테고리 마우스 오버 이미지
 * 카테고리 서브 메뉴 출력
 */
//아래 함수 실행되지 않아도 module="Layout_category"모듈에서 최상위 카테고리는 반복됨 (메인진열)
 $(document).ready(function(){
//methods 변수에 객체 선
    var methods = {
//빈 배열 aCategory
//빈 객체 aSubCategory
        aCategory    : [],
        aSubCategory : {},
//함수 get (methods.get())
        get: function()
        { 
//ajax 통신 (front에서 카테고리 받아옴)
             $.ajax({
                url : '/exec/front/Product/SubCategory',
                dataType: 'json',
                success: function(aData) {
//aData로 카테고리 객체 수신
//{
//    "link_product_list": "/category/카테고리명/",
//    "name": "카테고리명",
//    "param": "?cate_no=542",
//    "cate_no": 542,
//    "parent_cate_no": 535,
//    "design_page_url": "product/list.html",
//    "like_count": 0
//  },
//data값이 없으면 return false
                    if (aData == null || aData == 'undefined') return;
//반복문으로 aData의 length 만큼 반복
                    for (var i=0; i<aData.length; i++)
                    {
//sParentCateNo에 aData의 부모 카테고리 번호 할당
                        var sParentCateNo = aData[i].parent_cate_no;
//만약 위에 선언한 aSubcategory에서 부모카테고리 번호를 key로 하고 false면 (값이 없으면? 0이면? undefined 면?)
                        if (!methods.aSubCategory[sParentCateNo]) {
//해당 값을 빈 배열로 할당
                            methods.aSubCategory[sParentCateNo] = [];
                        }
//aSubCategory 객체에 aData를 key값에 맞게 Push
                        methods.aSubCategory[sParentCateNo].push( aData[i] );
                    }
                }
            });
        },
//링크 걸 parameter 함수
//iCateNo = Number(methods.getParam($this.find('a').attr('href'), 'cate_no'));
//마우스오버 한 부모 카테고리의 a태그의 href 주소와 cate_no를 인자로 함
//<a href="/product/list.html?cate_no=102">패션</a>
        getParam: function(sUrl, sKey) {
//?이후의 cate_no를 가져옴
            var aUrl         = sUrl.split('?');
//sQueryString 'cate_no=102'
            var sQueryString = aUrl[1];
//aParam이라는 빈 객체 선언
            var aParam       = {};
//만약 sQueryString 이 빈 값이 아니면
            if (sQueryString) {
//aFields라는 변수에 sQueryString에 &이 있는지 확인하고 구분자로 하여 배열로 다시 변환 ['cate_no=102']
                var aFields = sQueryString.split("&");
//aField라는 변수에 빈 배열 선언
                var aField  = [];
//aFields 의 길이만큼 반복
                for (var i=0; i<aFields.length; i++) {
//aField라는 빈 배열에 aFields의 원소를 f를 구분자로 하여 배열에 넣음 ['cate_no', '102']
                    aField = aFields[i].split('=');
//aParam이라는 빈 객체에 cate_no를 key 값으로 102를 value로 할당 {cate_no: '102'}
                    aParam[aField[0]] = aField[1];
                }
            }
//함수가 실행되면 sKey 인자가 들어왔는지 확인하고 (cate_no)있으면 aParam({cate_no: '102'})객체에서 value를 반환하고 아니면 객체를 반환(빈 객체?)
            return sKey ? aParam[sKey] : aParam;
        },
//html 만드는 함수
//마우스 오버한 카테고리 node와 getParam으로 받아온 카테고리 숫자 (102)
//methods.show($this, iCateNo);
        show: function(overNode, iCateNo) {
//aSubCategory의 key값이 지금 마우스 오버 한 카테고리의 숫자
//해당 카테고리의 value값이 빈 배열이면 return false
            if (methods.aSubCategory[iCateNo].length == 0) {
                return;
            }
//aHtml 이라는 빈 배열 생성
            var aHtml = [];
//aHtml에 <ul> 열기
            aHtml.push('<ul>');
//현재 마우스 오버 한 카테고리 숫자를 key로 한 배열의 인자를 하나씩 돌면서 함수 실행
            $(methods.aSubCategory[iCateNo]).each(function() {
//aHtml['<ul>'] 다음부터 li 생성
//aData 내 "design_page_url": "product/list.html" 과 "param": "?cate_no=543", "name": "카테고리명",
                aHtml.push('<li><a href="/'+this.design_page_url+this.param+'">'+this.name+'</a></li>');
            });
//마지막으로 <ul> 닫기
            aHtml.push('</ul>');
//마우스 오버한 카테고리 node 위치 잡고
            var offset = $(overNode).offset();
//sub-category div 만들기
            $('<div class="sub-category"></div>')
//카테고리 node의 offset 위치에 넣기
                .appendTo(overNode)
//배열로 [<ul>, <li>, </li>, </ul>]를 html메소드로 삽입 (join으로 합침) 
                .html(aHtml.join(''))
//find 메소드로 li에 마우스 올리면
                .find('li').mouseover(function(e) {
//클래스에 over 추가
                    $(this).addClass('over');
//마우스 빼면
                }).mouseout(function(e) {
//over 클래스 빼기
                    $(this).removeClass('over');
                });
        },
//닫기 함수
        close: function() {
//실행하면 위에서 만든 sub-category div를 지움 (subcategory를 삭제)
            $('.sub-category').remove();
        }
    };

//ajax 로 JSON 받아옴
    methods.get();

//실제 실행 부분
//Layout-categoey 모듈의 li 부분에 마우스가 올라가면
    $('.xans-layout-category li').mouseenter(function(e) {
//li 에 on 클래스 추가 (subcategory 로직 실행)
          var $this = $(this).addClass('on'),
//iCateNo 변수에 최상위 카테고리 숫자 할당
            iCateNo = Number(methods.getParam($this.find('a').attr('href'), 'cate_no'));
//카테고리 숫자가 없으면 return false;
          if (!iCateNo) {
               return;
          }
//마우스 올리면 show 함수 실행(위치 잡고 ul부터 join으로 append)
          methods.show($this, iCateNo);
//마우스 빼면 on 클래스 빼고 close 함수 실행
     }).mouseleave(function(e) {
        $(this).removeClass('on');
          methods.close();
     });
});

 

 

여기서 변수를 선언하는 다른 방식을 배워서 재밌게 개발했다.

 

var methods = {
   aCategory : {},
   get : function(){
   }
}

 

 

 

이렇게 선언하게 되면 methods.aCategory로 객체를 부를 수 있고 methods.get으로 function 을 이용할 수 있다.

 

이번에 개발한 것은 let methods에서 get 하는 함수를 선언하고 fetch를 이용해서 .then을 이용해서 모든 카테고리가 미리 불러와진 다음에 클릭하면 아코디언형 메뉴로 하위 카테고리가 보이고 안보이게 하는 방식을 채택했다.

클릭할 때 하위 카테고리가 불러와지는 기존 방식보다 다 미리 띄워놓고 display: none 으로 조절하는 것이 더 깔끔하다고 생각해서 (어차피 닫을 때 데이터를 삭제하는 것이 아니라 그냥 노드를 삭제하는것이기 때문에) 이 방식을 선택했다.

 

아래는 내가 커스텀 한 코드인데 기존 마우스오버 이벤트는 빼고 클릭 이벤트로 개발했고 이벤트는 addEventListener로 주었다.

제이쿼리를 완전히 빼고 바닐라 자바스크립트로 개발한 코드이기 때문에 좀 더 지저분 한 건 어쩔수없긔..

가 아니고 메소드 명이 너무 길어져서... (+슬라이드 클래스명까지 넣으니 줄줄이 소세지여)

 

 

 

 

 

let methods = {
  categoryObj: {},
  productCategoryArray: [],
  createdCategoryObj: function () {
    //category JSON 서버 통신
    fetch(`/exec/front/Product/SubCategory`, {
      method: "GET",
    })
      .then((response) => response.json())
      .then((data) => {
        if (data === null || data === "undefined") return false;
        for (let idx = 0; idx < data.length; idx++) {
          let parentCategoryNo = data[idx].parent_cate_no;
          if (!methods.categoryObj[parentCategoryNo]) {
            methods.categoryObj[parentCategoryNo] = [];
          }
          methods.categoryObj[parentCategoryNo].push(data[idx]);
        }
      })
      .then(() => {
        /* 최상위 대 카테고리 생성 */
        //createdCategoryObjResult 객체의 1번 key value 가져와서 parentCategoryArray로 생성
        const parentCategoryArray = methods.categoryObj[1];
        //parentCategoryArray로 반복하여 productCategoryArray,tagCategoryArray 배열에 각각 push (207은 All 카테고리 번호로 상품별과 가치태그 카테고리 구분점으로 지정)
        parentCategoryArray.forEach((parent) => {
            methods.productCategoryArray.push(parent);
        });
        //상품별 카테고리 대 카테고리 li 생성해서 insertAdjacentHTML 메소드를 이용해 UI를 생성한다.
        methods.productCategoryArray.forEach((productcategory) => {
          document
            .querySelector("#categoryOfProductList")
            .insertAdjacentHTML(
              "beforeend",
              `<li class="parent_category_${
                productcategory.cate_no
              }" data-cateno="${productcategory.cate_no}">
              <a href="product/list.html${productcategory.params}">
              ${productcategory.name}</li>`
            );
        });
      })
      .then(() => {
        // 대 카테고리 > 개별 중 카테고리 생성
        [
          ...document.querySelectorAll(
            "ul[id^='categoryOfProduct']>li[class^='parent_category']"
          ),
        ].forEach((parent) => {
          let middleCategory = methods.createdMidCategoryNode(
            parent.dataset.cateno,
            "category_product",
            "no-swiper"
          );
          parent.insertAdjacentHTML("afterend", middleCategory);
        });
        // 상품별 카테고리 > 개별 중 카테고리 > 개별 소 카테고리 생성
        [
          ...document.querySelectorAll(
            "ul[id^='categoryOfProduct']>li[class^='middle_main_category']
            >ul>li[class^='middle_category']"
          ),
        ].forEach((middle) => {
          let smallCategory = methods.createdProductSmallCategoryNode(
            middle.dataset.cateno
          );
          middle.insertAdjacentHTML("beforeend", smallCategory);
        });
      })
    return methods.categoryObj;
  },
  createdMidCategoryNode: function (
    parentCategoryNo,
    categoryType,
    boolSwiper
  ) {
    //중 카테고리 생성함수
    if (
      methods.categoryObj[parentCategoryNo] == undefined ||
      methods.categoryObj[parentCategoryNo].length === 0
    ) {
      return "";
    }
    //node 리스트의 ul 열기
    let categoryListNodeArray = [
      `<li class="middle_main_category_${parentCategoryNo} ${categoryType}">
      <ul class="${boolSwiper}">`,
    ];
    //node 리스트의 li 반복 생성
    methods.categoryObj[parentCategoryNo].forEach((el) => {
      categoryListNodeArray.push(`
            <li class="middle_category_${
              el.cate_no
            } swiper-slide" data-cateno="${el.cate_no}">
                <a href="/${el.design_page_url}${el.param}">
                    ${el.name}
                </a>
            </li>`);
    });
    //node 리스트의 ul 닫기
    categoryListNodeArray.push(`</ul></li>`);
    //최종 태그 리턴
    return categoryListNodeArray.join("");
  },
  createdProductSmallCategoryNode: function (midCategoryNo) {
    //소 카테고리 생성 함수
    if (
      methods.categoryObj[midCategoryNo] == undefined ||
      methods.categoryObj[midCategoryNo].length === 0
    ) {
      return "";
    }
    //node 리스트의 ul 열기
    let categoryListNodeArray = [
      `<ul class="small_main_category_${midCategoryNo} displaynone small_main">`,
    ];
    //node 리스트의 li 반복 생성
    methods.categoryObj[midCategoryNo].forEach((el) => {
      categoryListNodeArray.push(`
            <li class="small_category_${el.cate_no}" data-cateno="${el.cate_no}">
                <div class="category_link_wrap">
                    <a href="/${el.design_page_url}${el.param}">
                    ${el.name}
                    </a>
                </div>
            </li>`);
    });
    //node 리스트의 ul 닫기
    categoryListNodeArray.push(`</ul>`);
    //최종 태그 리턴
    return categoryListNodeArray.join("");
  },
};


/* 0. 통신 실행 / ul 아래 대, 중, 소 카테고리 생성 */
const createdCategoryObjResult = methods.createdCategoryObj();


/* 클릭 이벤트 */
window.addEventListener("load", () => {
  // 상품별 카테고리 > 중 카테고리 클릭 시 소 카테고리 보이기
  let productMidCategory = document.querySelectorAll(
    "ul[id^='categoryOfProduct'] li[class^='middle_category']"
  );
  productMidCategory.forEach((mid) => {
    mid.addEventListener("click", (e) => {
      if (e.currentTarget.childNodes[3]) {
        //소 카테고리 보이기
        e.currentTarget.childNodes[3].classList.toggle("displaynone");
        //subactive 넣기
        e.currentTarget.classList.toggle("sub_active");
      }
    });
  });

 

요즘 innerHTML이나 제이쿼리의 append() 는 잘 안 쓰고 insertAdjacentHTML 메소드를 더 잘 이용한다 (속도가 더 빠르다고 한다..)

그리고 category.js는 초반 반복문으로 객체를 만드는 것만 이해해도 ...

아래 함수는 100% 커스텀이기 때문에 JSON 가공만 잘 되면 카페24 카테고리도 어렵지 않다!

 

 

728x90