(주) 빌리고
Category
개발Customer
빌리고Date
2022내용
서버 관리, 코드 최적화, 관리자 개발, 프론트 앤드 개발1. 메인페이지 데이터 비동기 작업
카테고리 순서 관리 기능 구현:
관리자 페이지를 생성하여 카테고리 순서를 변경할 수 있는 인터페이스를 제공합니다.
Handlebars.js를 활용하여 관리자가 카테고리 순서를 동적으로 조정할 수 있는 기능을 추가합니다.
변경된 순서는 서버에 저장되어 메인 페이지에 적용됩니다.
스크롤 이벤트에 의한 데이터 동적 로드 구현:
ScrollToPlugin 및 ScrollMagic.js를 사용하여 스크롤 이벤트를 감지하고, 일정 위치에 도달했을 때 추가 데이터를 비동기적으로 로드합니다.
이를 위해 스크롤 위치에 따라 서버로부터 데이터를 요청하는 API 엔드포인트를 생성하고, 해당 데이터를 페이지에 동적으로 추가합니다.
ScrollMagic.js를 활용하여 스크롤 위치를 감지하고, 스크롤 이벤트를 트리거합니다.
성능 최적화 및 사용자 경험 개선:
이미지나 데이터 로딩 시 적절한 캐싱 기법을 사용하여 성능을 최적화합니다.
웹 페이지 스크롤 시 자연스러운 경험을 제공하기 위해 애니메이션 및 로딩 효과를 추가합니다.
사용자가 로딩 중임을 인지할 수 있도록 UI/UX 요소를 개선하여 사용자 경험을 향상시킵니다.
js
const mobileMain = (function () { Handlebars.registerHelper('ifDivision', function (arg1) { return (((arg1 + 1) % 2) === 0); }); Handlebars.registerHelper('numberformat', function (number) { return new Intl.NumberFormat('ko-KR').format(number) }); Handlebars.registerHelper('isContain', function (str, contain) { return (str.indexOf(contain) > -1); }) let popup = $(".plan_popup_box, .dim"); let sectionBox = $(".main_cate_box_01"); let headerSlider = $('.main_visual > .swiper-container'); let bestCateNav = $('.main_bestCate'); let categorySectionItems = $('.main_module_best > .module_item'); let categoryData = [{"bestCategory":"029","bestTitle":"\uacb0\ud569\uc0c1\ud488<\/em>"},{"bestCategory":"008","bestTitle":"TV\/\ub514\uc9c0\ud138<\/em>"},{"bestCategory":"004","bestTitle":"\ub0c9\uc7a5\uac00\uc804<\/em>"},{"bestCategory":"009","bestTitle":"\uc138\ud0c1\uac00\uc804<\/em>"},{"bestCategory":"005","bestTitle":"\uac74\uac15\/\ubdf0\ud2f0<\/em>"},{"bestCategory":"001","bestTitle":"\uc0dd\ud65c\uac00\uc804<\/em>"},{"bestCategory":"002","bestTitle":"\uacc4\uc808\/\ud658\uacbd<\/em>"},{"bestCategory":"006","bestTitle":"\uc8fc\ubc29\uac00\uc804<\/em>"},{"bestCategory":"003","bestTitle":"\uac00\uad6c\/\uce68\ub300<\/em>"},{"bestCategory":"010","bestTitle":"\ub808\uc800\/\uc720\uc544\ub3d9<\/em>"},{"bestCategory":"043","bestTitle":"\uc5d0\uc5b4\ucee8<\/em>"},{"bestCategory":"0","bestTitle":"0\uc6d0\ub80c\ud0c8<\/em>"}]; let currentIdx = 0; let sectionData = categoryData[currentIdx]; let scene; let controller = new ScrollMagic.Controller(); let onLoading = []; let itemTemplate = Handlebars.compile($("#template-category-section").html()); let isLoading = false; function getItems(code, fn) { if ($('#' + code).find('.module_item').length > 0) return; if (typeof categoryData[currentIdx] === 'undefined') return; if(code != 0){ $.ajax({ type: 'GET', url: '/api/v2/models', data: {'ca_id': code, 'page_rows': 8}, dataType: 'json', global: false, beforeSend: function () { isLoading = true; }, success: function (res) { isLoading = false; fn(res); }, }); }else{ $.ajax({ type: 'GET', url: '/api/v2/models', data: {'ca_id': code, 'page_rows': 8}, dataType: 'json', global: false, beforeSend: function () { isLoading = true; }, success: function (res) { isLoading = false; fn(res); }, }); } } function drawItems(data) { if (typeof categoryData[currentIdx] === 'undefined') return; let offsetObj = $('#' + data.ca_id); offsetObj.find('.page-load-wrap').show(); let sectionHTML = itemTemplate({'sectionData': sectionData, 'Lists': data.Lists}); offsetObj.find('.page-load-wrap').hide(); sectionBox.find('#' + data.ca_id).append(sectionHTML); } function initSwiper() { headerSlider = new Swiper(headerSlider, { slidesPerView: 1, loop: true, speed: 500, pagination: { el: '.swiper-pagination', type: 'fraction', }, autoplay: { delay: 4000, disableOnInteraction: false, }, }); categorySectionItems = new Swiper(categorySectionItems, { slidesPerView: 'auto', spaceBetween: 10, freeMode: true, }); bestCateNav = new Swiper(bestCateNav, { slidesPerView: 'auto', spaceBetween: 10, freeMode: true, }); } function initTrigger() { // Run through all sections let items = $('.main_cate_box_01 .main_module_best'); $.each(items, function (key, val) { let height = $(val).height(); //height of current element scene = new ScrollMagic.Scene({ duration: height + 5, triggerElement: val, triggerHook: 1, reverse: true }) .on("enter", function (event) { // categoryNav.slideTo(key, 500); if (onLoading.indexOf(categoryData[key].bestCategory) > -1) return; onLoading.push(categoryData[key].bestCategory); getItems(categoryData[key].bestCategory, function (data) { drawItems(data); currentIdx++; }); }) // .addIndicators({name: " - begin "}) // Testing .addTo(controller); }) } function initSection() { let requests = []; for (const categoryDatum of categoryData) { requests.push(new Promise(done => { getItems(categoryDatum.bestCategory, function (data) { drawItems(data); }); return done; })); } Promise.all(requests); } // 쿠키 가져오기 let getCookie = function (cname) { let name = cname + "="; let ca = document.cookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) === ' ') c = c.substring(1); if (c.indexOf(name) !== -1) return c.substring(name.length, c.length); } return ""; } // 24시간 기준 쿠키 설정하기 let setCookie = function (cname, cvalue, exdays) { let todayDate = new Date(); todayDate.setTime(todayDate.getTime() + (exdays * 24 * 60 * 60 * 1000)); let expires = "expires=" + todayDate.toUTCString(); document.cookie = cname + "=" + cvalue + "; " + expires; } let couponClose = function () { if (popup.find("input[type=checkbox]").prop("checked") === true) { setCookie("close", "Y", 1); //기간( ex. 1은 하루, 7은 일주일) } popup.hide(); } function openPop() { let cookiedata = document.cookie; if (cookiedata.indexOf("close=Y") < 0) { popup.show(); } else { popup.hide(); } popup.find('.off_box').click(function () { couponClose(); }); } return { init: function () { $(function () { initTrigger(); // initSection(); openPop(); }); } }; })(jQuery); mobileMain.init();
html
<div class="main_cate_box_01">
<div id="029" class="section">
<div class="main_prodList inner">
<div class="prodList_tit">
<h2 class="ff_NSR"><span><em>결합상품</em></span></h2>
</div>
<div class="prodList_list prod_list ">
<div class="main_module_best">
</div>
</div>
<div class="page-load-wrap section">
<div class="page-load"></div>
<p class="ff_NSR"><em>로딩중입니다.</em><br>잠시만 기다려 주세요 :)</p>
</div>
</div>
</div>
</div>
<div class="main_prodList inner">
<div class="prodList_list prod_list">
<ul class="col4">
{{#each Lists}}
<li>
<div class="prod_box box sdw rnd12">
<a href="{{this.model_url}}">
<div class="photo">
<img src="{{this.model_thumnail_url}}" />
<div class="flag_water_wrap">{{{this.model_icons}}}</div>
</div>
<div class="info">
{{#if model_icons}}
<div class="flag_wrap">{{{this.model_icons}}}</div>
{{/if}}
<div class="prd_tit">
<span class="model ff_Play">{{this.model}}</span>
<h3>{{this.model_name}}</h3>
</div>
<div class="prd_prc ff_Play">
<dl class="prc">
<dt>{{this.model_monthly_string}}</dt>
<dd><em>{{numberformat this.goods_price}}</em>원</dd>
</dl>
<dl class="card">
<dt>{{this.model_alliance_string}}</dt>
<dd><em>{{numberformat this.model_sale_price}}</em>원</dd>
</dl>
</div>
</div>
</a>
</div>
</li>
{{/each}}
</ul>
</div>
</div>
2. 비동기식 카테고리 필터
HTML
{{#each FilterList}}
{{#if (isNotEmpty value)}}
{{#if (isSame param 'catecode')}}<!-- 1차카테고리 -->
<div class="ft_row"><h3>{{ name }}</h3>
<ul>
{{#each value}}
<li class="{{../class}}">
<a href="javascript:;" data-target="filter_{{../param}}%5B%5D" data-value="{{@key}}" data-multiple="{{../isMultiple}}">{{catename}}</a>
</li>
{{/each}}
</ul>
</div>
<div class="ft_row sub" style="display:none;"><h3>하위 카테고리</h3>
<ul>
{{#each value}}
{{#each children}}
<li class="parent_{{upcate}}" style="display:none;">
<a href="javascript:;" data-target="filter_catecode%5B%5D" data-value="{{catecode}}" data-multiple="{{../isMultiple}}">{{catename}}</a>
</li>
{{/each}}
{{/each}}
</ul>
</div>
{{else}}
<div class="ft_row" data-display="30"><h3>{{ name }}</h3>
<ul class="right_list">
{{#each value}}
{{#if ../isReorder }}
<li class="js-load item {{../class}} {{#compare @index 30 operator='<'}} {{/compare}} {{ isImportant ../this id }}"><a href="#" data-target="filter_{{../param}}%5B%5D" data-value="{{id}}" data-multiple="{{../isMultiple}}">{{{name}}}</a></li>
{{else}}
<li class="js-load item {{../class}} {{#compare @index 30 operator='<'}} {{/compare}} {{ isImportant ../this @key }}"><a href="#" data-target="filter_{{../param}}%5B%5D" data-value="{{@key}}" data-multiple="{{../isMultiple}}">{{{this}}}</a></li>
{{/if}}
{{/each}}
</ul>
{{#compare value 30 operator='>'}}
<div class="btn-wrap btn_more active"> <a href="javascript:;" class="button"><span>더보기</span> <i class="ico_arrow_down_b"></i></a> </div>
{{/compare}}
</div>
{{/if}}
{{/if}}
{{/each}}
js
var regexPage = /&page=\w+/g;
var regexFilter = /&filter_[a-z]+(\[\]|%5B%5D)=+[A-z,\|,\,,\*,\-,0-9]+/g;
// 정규식부분 처리해야함.
var regexSort = /&sort=+[A-z, \.]+/g;
var regexSorder = /&sortodr=+[A-z]+/g;
var filterAttr = [];
var enableFilter = false;
var onSub = false;
Handlebars.registerHelper('numberformat', function (val) {
return parseInt(val).toLocaleString();
})
Handlebars.registerHelper('isNotEmpty', function (obj) {
return (obj !== null);
})
Handlebars.registerHelper('isSame', function (val1, val2) {
return (val1 == val2);
})
Handlebars.registerHelper('isContain', function (str, contain) {
return (str.indexOf(contain) > -1);
})
Handlebars.registerHelper('isImportant', function (data, val) {
if (typeof data.highlight !== 'undefined') {
return (data.highlight.indexOf(parseInt(val)) > -1) ? " filter_important":"";
}
})
Handlebars.registerHelper('compare', function(lvalue, rvalue, options) {
if (typeof lvalue === 'object') {
lvalue = Object.keys(lvalue).length;
}
if (arguments.length < 3)
throw new Error("Handlerbars Helper 'compare' needs 2 parameters");
operator = options.hash.operator || "==";
var operators = {
'==': function(l,r) { return l == r; },
'===': function(l,r) { return l === r; },
'!=': function(l,r) { return l != r; },
'<': function(l,r) { return l < r; },
'>': function(l,r) { return l > r; },
'<=': function(l,r) { return l <= r; },
'>=': function(l,r) { return l >= r; },
'typeof': function(l,r) { return typeof l == r; }
}
if (!operators[operator])
throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);
var result = operators[operator](lvalue,rvalue);
if( result ) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
Handlebars.registerHelper('displayFuneralServiceAgency', function (goodsType) {
return (goodsType === 'F');
});
$(function() {
// Filter Reset
$("#filterReset").click(function() {
$(".filter_area > .ft_row li").removeClass("on");
$(".filter_area > .ft_row.sub li").removeClass("on");
$(".filter_area > .ft_row.sub").hide();
var renewUrl = location.href;
renewUrl = renewUrl.replace(regexPage, "");
renewUrl = renewUrl.replace(regexFilter, "");
// Parameter & url setting
var parameter = renewUrl.split("?");
history.pushState(null, null, renewUrl);
// Models Requests Lists
modelsRequests(parameter[1]);
})
});
// Models Requests Lists
function modelsRequests(obj) {
var Parameters = new Array();
if($.type(obj) == "object" || $.type(obj) == "array") {
$.each(obj, function(key, val) {
Parameters.push(key + "=" + val);
});
}
else if($.type(obj) == "string") {
$.each(obj.split("&"), function(key, val) {
Parameters.push(val);
});
}
Parameters.push("section=models")
var ResponseData = "";
//Ajax call
var AjaxUrl = "/api/v2/models";
var AjaxParameter = Parameters.join("&");
var AjaxType = "GET";
var AjaxDataType = "JSON";
var AjaxAsync = false;
var AjaxError = "";
var AjaxComplete = "";
var AjaxSuccess = function(data) {
modelsResponse(data);
//ResponseData = data;
};
AjaxLoadAni(AjaxUrl, AjaxParameter, AjaxType, AjaxDataType, AjaxAsync, AjaxSuccess, AjaxError, AjaxComplete);
return ResponseData ? ResponseData : "";
}
function initSmartFilter(data) {
if(enableFilter) return;
for (let i = 0; i < data["FilterList"].length; i++) {
if (data["FilterList"][i]["name"] == "상조사") {
const tmp = data["FilterList"][0];
data["FilterList"][0] = data["FilterList"][1];
data["FilterList"][1] = tmp;
}
}
var template = Handlebars.compile($("#template-filter-section").html());
$('.smart_filter .filter_area').html(template(data));
const subRow = $('.ft_row.sub');
// Smart Filter
$('.ft_row > ul > li a').click(function(e) {
e.preventDefault();
var self = $(this);
var isOn = $(this).parent('li').hasClass('on');
var parentVal = $(this).data('parent');
var selectedVal = $(this).data('value');
var selectedTarget = $(this).data('target');
var isMultiple = $(this).data('multiple');
$(this).parent('li').toggleClass('on');
var renewUrl = location.href;
renewUrl = renewUrl.replace(regexPage, "");
renewUrl = renewUrl.replace(regexFilter, "");
if (isMultiple != true) {
if ($(this).closest('ul').find('li.on').length > 1) {
$(this).closest('ul').find('li').removeClass('on');
$(this).parent('li').toggleClass('on');
}
}
if (selectedTarget === 'filter_catecode%5B%5D' || selectedTarget === 'filter_catecode[]') {
if ($(this).closest('ul').find('li.on').length > 0 && selectedVal !== "059" && selectedVal !== "070") {
if (!$(this).closest('.ft_row').hasClass('sub')) {
subRow.find('li').removeClass('on').hide();
subRow.show();
subRow.find('.parent_' + selectedVal).stop().fadeIn();
}
}else if(!$(this).closest('.ft_row').hasClass('sub')) {
subRow.find('li').removeClass('on').hide();
subRow.hide();
}
}
var filterParameter = new Array();
if (parentVal) {
var attr = _.find(filterAttr, parentVal);
if (isOn) {
if (typeof attr !== 'undefined') {
var filterAttrIndex = _.findIndex(filterAttr, parentVal);
attr[parentVal].push(selectedVal);
filterAttr[filterAttrIndex] = attr;
}else{
var data = {};
data[parentVal] = [parseInt(selectedVal)];
filterAttr.push(data)
}
}else{
_.each(filterAttr, function (attr) {
if (_.has(attr, parentVal)) {
_.remove(attr[parentVal], function (v) {
return v === selectedVal;
});
if (_.isEmpty(attr[parentVal])) {
_.remove(filterAttr, attr);
}
}
});
}
filterAttr = filterAttr.filter(function(){return true;});
var attrParams = "";
var result = _.chain(filterAttr)
.map(function(n){
return _.first(_.keys(n))+"|"+_.join(_.values(n));
})
.join('**').value();
filterParameter.push(selectedTarget + "=" +result);
if(filterAttr.length < 1){
var attrIndex = filterParameter.indexOf('filter_attr%5B%5D=');
filterParameter.splice(attrIndex,1)
} //&& _.findIndex(filterParameter,'filter_attr[]=') > -1) renewUrl = renewUrl.replace("filter_attr[]=", "");
}else{
let isChecked = false;
$.each($(".filter_area > .ft_row li"), function(key, val) {
if($(this).hasClass("on") === true) {
var dataTatget = $(this).find("a").data("target");
var dataValue = $(this).find("a").data("value");
var dataParent = $(this).find("a").data("parent");
filterParameter.push(dataTatget + "=" +dataValue);
isChecked = true;
}
});
if (!isChecked) {
subRow.find('li').hide();
subRow.hide();
}
$.each($(".filter_area > .ft_row.sub li"), function(key, val) {
if($(this).hasClass("on") === true) {
var dataTatget = $(this).find("a").data("target");
var dataValue = $(this).find("a").data("value");
var dataParent = $(this).find("a").data("parent");
if (dataTatget === 'filter_catecode%5B%5D' || dataTatget === 'filter_catecode[]') {
if (subRow.find('.parent_' + dataValue).length > 0) {
let children = null;
subRow.find('.parent_' + dataValue).each(function () {
if ($(this).hasClass("on") === true) {
children = $(this).children("a");
}
});
if (children !== null) {
dataTatget = children.data('target');
dataValue = children.data('value');
filterParameter.push(dataTatget + "=" + dataValue);
onSub = true;
return false;
}
}
}
}
});
}
if(filterParameter.length > 0) renewUrl = renewUrl + "&" + filterParameter.join("&");
// Parameter & url setting
var parameter = renewUrl.split("?");
history.pushState(null, null, renewUrl);
// Models Requests Lists
modelsRequests(parameter[1]);
});
// Sorting Models
$('.sorting > ul > li').click(function(e) {
e.preventDefault();
$(this).parent('li label').toggleClass('on');
var renewUrl = location.href;
renewUrl = renewUrl.replace(regexPage, "");
renewUrl = renewUrl.replace(regexFilter, "");
renewUrl = renewUrl.replace(regexSort, "");
renewUrl = renewUrl.replace(regexSorder, "");
var filterParameter = new Array();
// Check Filter
$.each($(".filter_area > .ft_row li"), function(key, val) {
if($(this).hasClass("on") === true) {
var dataTatget = $(this).children("a").attr("data-target");
var dataValue = $(this).children("a").attr("data-value");
filterParameter.push(dataTatget + "=" + dataValue);
}
});
// Check Sort
$.each($(".sorting > ul > li"), function(key, val) {
if($(this).find("label").hasClass("on") === true) {
var dataTatget = $(this).attr("data-target");
var dataValue = $(this).attr("data-value");
if(dataTatget && dataValue) filterParameter.push("sort=" + dataTatget + "&sortodr=" + dataValue);
}
});
if(filterParameter.length > 0) renewUrl = renewUrl + "&" + filterParameter.join("&");
// Parameter & url setting
var parameter = renewUrl.split("?");
history.pushState(null, null, renewUrl);
// Models Requests Lists
modelsRequests(parameter[1]);
});
enableFilter = true;
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const
keys = urlParams.keys(),
values = urlParams.values(),
entries = urlParams.entries();
for (const entry of entries) {
let targetObj = $(`[data-target="${entry[0]}"]`);
let valueObj = $(`[data-value="${entry[1]}"]`);
$('.smart_filter .filter_area .ft_row ul > li a').each(function (e) {
if (($(this).data('target') === entry[0] && $(this).data('value') == entry[1]) || $(this).data('target') === encodeURIComponent(entry[0]) && $(this).data('value') == encodeURIComponent(entry[1])) {
$(this).closest('li').addClass('on');
}
if (decodeURIComponent(entry[0]) == 'filter_catecode[]' && subRow.find('.parent_' + entry[1]).length > 0) {
subRow.find('li').hide();
subRow.show();
subRow.find('.parent_' + entry[1]).fadeIn();
}
});
}
// $('.btn_more').unbind('click').click(function(e){
// e.preventDefault();
// const row = $(this).closest('.ft_row');
// const displayCount = row.data('display');
// const list = row.find('.right_list');
// const activeCount = list.find('li.active').length;
// if (activeCount <= displayCount) {
// list.find('li').addClass('active');
// }else{
// list.find(`li:gt(${displayCount-1})`).removeClass('active');
// }
// if($(this).find('i').hasClass('ico_arrow_up_b')){
// $(this).find('i').removeClass('ico_arrow_up_b');
// $(this).find('i').addClass('ico_arrow_down_b');
// } else {
// $(this).find('i').removeClass('ico_arrow_down_b');
// $(this).find('i').addClass('ico_arrow_up_b');
// }
// $(this).find('span').text((activeCount > displayCount) ? '더보기' : '접기');
// });
const brand = $('.ft_row ul li:nth-child(1).brand').parent('ul');
const brandHeight = brand.outerHeight();
if (brandHeight > 96) {
brand.addClass('fold');
$('.btn_more').addClass('active');
$('.btn_more').click(function () {
if (brand.hasClass('fold')) {
brand.removeClass('fold');
$(this).find('span').text('접기');
$(this).find('i').addClass('ico_arrow_up_b');
$(this).find('i').removeClass('ico_arrow_down_b');
} else {
brand.addClass('fold');
$(this).find('span').text('더보기');
$(this).find('i').addClass('ico_arrow_down_b');
$(this).find('i').removeClass('ico_arrow_up_b');
}
});
}
}
var modelsResponse = function(data) {
if (data.Lists.length < 1) {
$('.prodList_none').show();
}else{
$('.prodList_none').hide();
}
var modelLists = "";
var template = Handlebars.compile($("#template-goods").html());
$(".prodList_wrap > .prodList_list > ul").html(template(data.Lists));
// Lists Counts Output
$(".prodList_wrap > .prodList_sort > h3 > .txt_org").html(data.Counts);
// Pagination Output
$(".prodList_wrap > .paging").html(data.Paginations);
initSmartFilter(data);
}
modelsRequests('ca_id=034');
//필터 접기/펴기 부분
//필터 접기/펴기 부분
$('.popup_close_btn').click(function (){
$('.list_popup_banner').fadeOut(300);
});
$('.list_best_item').slick({
slidesToShow: 4,
slidesToScroll: 4,
dots: true,
dotsClass: 'custom_paging ff_Play',
customPaging: function (slider, i) {
return '<em>' + (i + 1) + '</em>' + '/' + Math.ceil(slider.slideCount / 4);
}
});