백엔드 API 없이 필터 기능 구현하기

Dave·2023년 8월 21일
0
post-thumbnail

기능을 볼 수 있는 페이지 : 프로계획러 사용자가 직접 만든 풀코스 여행

1. 비동기식으로 데이터를 불러오기 위해 사용한 것

비동기 방식으로 데이터를 불러오기 위해 깃헙에 있는 저장공간을 사용했다. 깃헙에 모든 데이터를 담은 json 파일을 저장하고, 필터 기능이 사용될 때마다 요청하여 해당 저장공간에서 json 파일을 받아오는 식이다.

요청하는 URL은 다음과 같이 적용할 수 있다.

https://raw.githubusercontent.com/(저장소 소유자 이름)/(저장소 이름)/(브랜치명)/(json 파일 명).json

저장된 데이터의 객체 데이터 구조는 다음과 같다.

{
 "category":"family",
 "duration":"2",
 "Title": "부산 여행",
 "Title_link": "https://korean.visitkorea.or.kr/detail/cs_detail_user.do?crsid=55b7d2ff-c287-41ec-bd51-c5ac6e0cf852",
 "Thumbnail": "https://cdn.visitkorea.or.kr/img/call?cmd=VIEW&id=c07403bc-2f2a-4247-92e8-f402ac3c6a7e",
 "지역": "부산 서구",
 "총거리": "63.7km",
 "area_course": "부산 송도해상케이블카",
 "area_course1": "아홉산숲",
 "area_course2": "부산 부전시장",
 "Avatar": "https://ssl.pstatic.net/static/pwe/address/img_profile.png",
 "area_course3": "부산 트릭아이뮤지엄",
 "userInfo": "j*******",
 "area_course4": "BIFF 광장",
 "area_course5": "스파랜드 센텀시티"
}

이후 필터 기능은 해당 데이터에서 category 프로퍼티, duration 프로퍼티를 통해 분류할 예정이다.

2. HTML 코드

<ul class="courseUser_filterList">
    <li class="courseUser_filterListItem">
        <a class="courseUser_filterListItemLink" data-category="family" href="#none">
            <span class="courseUser_filterListItemLinkText">가족코스</span>
        </a>
    </li>
    <li class="courseUser_filterListItem">
        <a class="courseUser_filterListItemLink" data-category="alone" href="#none">
            <span class="courseUser_filterListItemLinkText">혼자여행</span>
        </a>
    </li>
    <li class="courseUser_filterListItem">
        <a class="courseUser_filterListItemLink" data-category="walk" href="#none">
            <span class="courseUser_filterListItemLinkText">도보코스</span>
        </a>
    </li>
    <li class="courseUser_filterListItem">
        <a class="courseUser_filterListItemLink" data-category="healing" href="#none">
            <span class="courseUser_filterListItemLinkText">힐링코스</span>
        </a>
    </li>
    <li class="courseUser_filterListItem">
        <a class="courseUser_filterListItemLink" data-category="food" href="#none">
            <span class="courseUser_filterListItemLinkText">맛코스</span>
        </a>
    </li>
    <li class="courseUser_filterListItem">
        <a class="courseUser_filterListItemLink" data-category="camping" href="#none">
            <span class="courseUser_filterListItemLinkText">캠핑코스</span>
        </a>
    </li>
    <li class="courseUser_filterListItem">
        <a class="courseUser_filterListItemLink" data-category="pet" href="#none">
            <span class="courseUser_filterListItemLinkText">반려동물과 함께</span>
        </a>
    </li>
</ul>

특정 구역이 클릭 됐을 때 해당 카테고리를 갖도록 필터를 주기 위해 목록 태그에 data-set 어트리뷰트를 줬다.

<ul class="courseUser_filterBarList">
    <li class="courseUser_filterBarListItem">
        <a data-duration="all" class="courseUser_filterBarListItemLink" href="#none">
            <span class="courseUser_filterBarListItemLinkText">전체</span>
        </a>
    </li>
    <li class="courseUser_filterBarListItem">
        <a data-duration="dayTrip" class="courseUser_filterBarListItemLink" href="#none">
            <span class="courseUser_filterBarListItemLinkText">당일여행</span>
        </a>
    </li>
    <li class="courseUser_filterBarListItem">
        <a data-duration="1N2D" class="courseUser_filterBarListItemLink" href="#none">
            <span class="courseUser_filterBarListItemLinkText">1박 2일</span>
        </a>
    </li>
    <li class="courseUser_filterBarListItem">
        <a data-duration="2N3D" class="courseUser_filterBarListItemLink" href="#none">
            <span class="courseUser_filterBarListItemLinkText">2박 3일 이상</span>
        </a>
    </li>
</ul>

여행 기간에 대한 필터링도 마찬가지로 data-set 어트리뷰트를 사용했다.

3. 자바스크립트 코드

일단 모든 요청마다 데이터를 모두 불러온 다음 그 데이터의 프로퍼티의 category, duration 값이 필터의 값과 일치한 경우의 데이터만 출력되도록 한다.

3-1. 변수 선언

// 선택된 카테고리
let currentCategory;
// 선택된 여행 기간
let currentDuration;
// AJAX 호출 후 받은 데이터를 담을 배열
let dataArray;
// 필터된 데이터를 받을 배열
let filteredData;
// 여행 일수에 대한 필터를 거친 데이터를 받을 배열
let durationFilteredData
// 카테고리 선택 버튼 요소
const userCourseCategoryButtons = document.querySelectorAll('.courseUser_filterListItemLink');
// 여행기간 선택 버튼 요소
const userCourseDurationButtons = document.querySelectorAll('.courseUser_filterBarListItemLink')
// 유저가 선택한 코스 아이템
const userCourseListItems = document.querySelectorAll('.courseUser_contentListItem');
// 코스 더보기 버튼
const userCourseMoreBottons = document.querySelectorAll('.courseUser_contentListItemCoverGocourseLink');
// 코스 첫 화면
const userCourseProfiles = document.querySelectorAll('.courseUser_contentListItemCover_box');
// 코스에서 유저가 더보기를 클릭했을 때 화면
const userCourseRoutes = document.querySelectorAll('.courseUser_contentListItemInfo_box');
// 페이지네이션 버튼 박스
const buttonBox = document.querySelector('.userCourse_pageButton_box');
// 페이지 네이션에 필요한 변수
const itemsPerPage = 5;
let currentPage = 1;

3-2. 함수 선언

1) 데이터를 깃헙 저장소에서 비동기 식으로 가져오는 함수

async function getFetchedData(){
    const response = await fetch('https://raw.githubusercontent.com/(깃헙 사용자 이름)/(깃헙 저장소 이름)/(깃헙 브랜치 이름)/UserCourse.json');
    const jsonData = await response.json();
    dataArray = jsonData;
}

2) 페이지 별로 데이터를 분할 하는 함수

function paginateData(data, currentPage, itemsPerPage){
    const startIndex = (currentPage - 1) * itemsPerPage;
    const endIndex = startIndex + itemsPerPage;
    return data.slice(startIndex, endIndex);
    // 인수로 전달된 범위의 요소들을 복사하여 배열로 반환, 원본 배열은 변경되지 않는다.
    // ex) 현재 페이지 1 : 데이터 인덱스가 0 ~ 9번 인덱스 요소를 데이터로 잡음
    // ex) 현재 페이지 2 : 데이터 인덱스가 10 ~ 19번 인덱스 요소를 데이터로 잡음
}

data : 깃헙 저장소에서 가져온 전체 데이터
currentPage : 현재 페이지 인덱스
itemsPerpage : 한 페이지 당 표시할 아이템 개수

위 값을 이용하여 현재 페이지에 비례하여 startIndex 번째부터 endIndex 번째의 데이터를 가져온다.

3) 페이지네이션 버튼을 생성하는 함수

function createPageButtons(userCourseFilteredDataArray, totalPages){
    buttonBox.innerHTML='';
    
    const maxButtons = 10; // 한 번에 표시할 페이지 버튼의 최대 개수
    const maxPagesPerGroup = 10; // 그룹 당 표시할 페이지 수

    let startPage = Math.floor((currentPage-1)/maxPagesPerGroup) * maxPagesPerGroup + 1;
    // 시작 페이지 = ( ( 현제 페이지 - 1 ) / 그룹 당 표시할 페이지 수  )을 내림한 값 * 그룹 당 표시할 페이지 수 + 1;
    // ex) 현재 페이지 : 5, {(5-1)/10} 내림한 값 = 0 * 10 + 1 = 1
    // ex) 현재 페이지 : 19, {(19-1)/10} 내림한 값 = 1 * 10 + 1 = 11
    let endPage = startPage + maxButtons - 1;
    // 마지막 페이지 = 시작 페이지 + 한 번에 표시할 페이지 버튼의 최대 개수 - 1;
    // ex) 시작 페이지 = 1, 1 + 10 - 1 = 10
    // ex) 시작 페이지 = 11, 11 + 10 - 1 = 20

    // 가장 마지막 페이지 그룹에 도달 시 마지막 페이지는 총 페이지 인덱스와 일치한다.
    if (endPage > totalPages){
        endPage = totalPages
        // 만약 마지막 페이지가 총 페이지 수 보다 넘칠 경우 마지막 페이지의 인덱스는 총 페이지 인덱스와 일치한다.
    }

    // 이전 버튼 생성
    if (currentPage > 1){ // 만약 현재 페이지가 1보다 크다면 실행한다.
        const prevButton = document.createElement('button');
        prevButton.className='userCourse_pageButton userCourse_pageButton--prev';
        prevButton.textContent = '이전';
        prevButton.addEventListener('click', ()=>{
            // 이전 버튼을 클릭할 시 현재 페이지 값을 1 감소시킨다.
            currentPage--;
            // 감소시킨 현재 페이지 인덱스에서 데이터를 호출한다.
            renderHTML(userCourseFilteredDataArray);
        });
        buttonBox.appendChild(prevButton);
    }

    // 페이지 버튼 생성
    for (let i = startPage; i <= endPage; i++){
        const button = document.createElement('button');
        button.className='userCourse_pageButton';
        button.textContent = i;

        // 현재 페이지와 일치하는 버튼에 클래스 부여
        if (i == currentPage){
            button.classList.add('userCourse_pageButton--on')
        }

        button.addEventListener('click', () => {
            currentPage=i;
            renderHTML(userCourseFilteredDataArray);
        });
        buttonBox.appendChild(button);
    }

    // 다음 버튼 생성
    if (currentPage < totalPages){
        const nextButton = document.createElement('button');
        nextButton.className='userCourse_pageButton userCourse_pageButton--next'
        nextButton.textContent='다음';
        nextButton.addEventListener('click', ()=>{
            currentPage++;
            renderHTML(userCourseFilteredDataArray);
        });
        buttonBox.appendChild(nextButton);
    }
}

페이지네이션 버튼을 생성하는 createPageButtons 함수는 필터된 데이터인userCourseFilteredDataArray와 총 페이지 수인 totalPages를 인수로 받는다.

buttonBox.innerHTML='';

버튼을 렌더링 하는 컨테이너 내부의 태그를 초기화한다.

const maxButtons = 10; // 한 번에 표시할 페이지 버튼의 최대 개수
const maxPagesPerGroup = 10; // 그룹 당 표시할 페이지 수

이후 기존 방식과 마찬가지로 버튼의 최대 개수, 한 페이징 그룹 당 표시할 페이지 수를 선언한다.

let startPage = Math.floor((currentPage-1)/maxPagesPerGroup) * maxPagesPerGroup + 1;
// 시작 페이지 = ( ( 현재 페이지 - 1 ) / 그룹 당 표시할 페이지 수  )을 내림한 값 * 그룹 당 표시할 페이지 수 + 1;
// ex) 현재 페이지 : 5, {(5-1)/10} 내림한 값 = 0 * 10 + 1 = 1
// ex) 현재 페이지 : 19, {(19-1)/10} 내림한 값 = 1 * 10 + 1 = 11
let endPage = startPage + maxButtons - 1;
// 마지막 페이지 = 시작 페이지 + 한 번에 표시할 페이지 버튼의 최대 개수 - 1;
// ex) 시작 페이지 = 1, 1 + 10 - 1 = 10
// ex) 시작 페이지 = 11, 11 + 10 - 1 = 20

이후 시작 페이지, 마지막 페이지 인덱스 값을 구한다.

if (endPage > totalPages){
    endPage = totalPages
    // 만약 마지막 페이지가 총 페이지 수 보다 넘칠 경우 마지막 페이지의 인덱스는 총 페이지 인덱스와 일치한다.
}

만약 마지막 페이지가 총 페이지보다 클 경우 마지막 페이지의 인덱스 값은 총 페이지 인덱스값으로 설정한다.

// 이전 버튼 생성
if (currentPage > 1){ // 만약 현재 페이지가 1보다 크다면 실행한다.
    const prevButton = document.createElement('button');
    prevButton.className='userCourse_pageButton userCourse_pageButton--prev';
    prevButton.textContent = '이전';
    prevButton.addEventListener('click', ()=>{
        // 이전 버튼을 클릭할 시 현재 페이지 값을 1 감소시킨다.
        currentPage--;
        // 감소시킨 현재 페이지 인덱스에서 데이터를 호출한다.
        renderHTML(userCourseFilteredDataArray);
    });
    buttonBox.appendChild(prevButton);
}

이전 버튼을 생성하는 부분은 이전과 동일하다.

// 페이지 버튼 생성
for (let i = startPage; i <= endPage; i++){
    const button = document.createElement('button');
    button.className='userCourse_pageButton';
    button.textContent = i;

    // 현재 페이지와 일치하는 버튼에 클래스 부여
    if (i == currentPage){
        button.classList.add('userCourse_pageButton--on')
    }

    button.addEventListener('click', () => {
        currentPage=i;
        renderHTML(userCourseFilteredDataArray);
    });
    buttonBox.appendChild(button);
}

페이지 버튼을 생성하는 부분도 이전 소개한 기능과 동일하다.

// 다음 버튼 생성
if (currentPage < totalPages){
    const nextButton = document.createElement('button');
    nextButton.className='userCourse_pageButton userCourse_pageButton--next'
    nextButton.textContent='다음';
    nextButton.addEventListener('click', ()=>{
        currentPage++;
        renderHTML(userCourseFilteredDataArray);
    });
    buttonBox.appendChild(nextButton);
}

다음 버튼 생성도 마찬가지로 동일하다.

4) 필터된 데이터를 처리하여 HTML을 렌더링하는 함수

function renderHTML(userCourseFilteredDataArray){
    grid.innerHTML='';
    const slicedData = paginateData(userCourseFilteredDataArray, currentPage, itemsPerPage);
    slicedData.forEach((userCourseData, ind)=>{
        const newLi = document.createElement('li');
        newLi.className='courseUser_contentListItem';
        newLi.insertAdjacentHTML('afterbegin', `<img class="courseUser_contentListItemBackgroundImg" src="${userCourseData.Thumbnail}">
                <div class="courseUser_contentListItemCover_box">
                    <a href="../course/courseContent.html">
                        <strong class="courseUser_contentListItemCoverTitle">${userCourseData.Title}</strong>
                        <p class="courseUser_contentListItemCoverRegion">
                            지역 :
                            <span>${userCourseData.지역}</span>
                        </p>
                        <p class="courseUser_contentListItemCoverDistance">
                            총거리 :
                            <span>${userCourseData.총거리}</span>
                        </p>
                    </a>
                    <div class="courseUser_contentListItemCoverUser_box">
                        <img class="courseUser_contentListItemCoverUserImg" src="${userCourseData.Avatar}" alt="">
                        <span class="courseUser_contentListItemCoverUserName">${userCourseData.userInfo}</span>
                    </div>
                    <div class="courseUser_contentListItemCoverGocourse_box">
                        <a class="courseUser_contentListItemCoverGocourseLink" href="#none">코스 더보기</a>
                    </div>
                </div>`);
        const newDiv = document.createElement('div');
        const newUl = document.createElement('ul');
        newDiv.classList.add('courseUser_contentListItemInfo_box');
        newDiv.classList.add('invisible');
        newUl.className='courseUser_contentListItemInfoList';
        
        for(let i=0; ; i++){
            if(i == 0){
                newUl.insertAdjacentHTML('beforeend', `<li class="courseUser_contentListItemInfoListItem">
                                        <a class="courseUser_contentListItemInfoListItemLink" href="#none">${userCourseData.area_course}</a>
                                    </li>`)
            } else if(userCourseData[`area_course${i}`]){
                newUl.insertAdjacentHTML('beforeend', `<li class="courseUser_contentListItemInfoListItem">
                                        <a class="courseUser_contentListItemInfoListItemLink" href="#none">${userCourseData[`area_course${i}`]}</a>
                                    </li>`)
            } else{
                break;
            }
        }

        newDiv.insertAdjacentElement('afterbegin', newUl);
        newLi.insertAdjacentElement('beforeend', newDiv);
        grid.appendChild(newLi);

        var msnry = new Masonry(grid, {
            itemSelector: '.courseUser_contentListItem',
            columnWidth: 298
        })

        const userCourseListItems = document.querySelectorAll('.courseUser_contentListItem');
        const userCourseMoreBottons = document.querySelectorAll('.courseUser_contentListItemCoverGocourseLink');
        const userCourseProfiles = document.querySelectorAll('.courseUser_contentListItemCover_box');
        const userCourseRoutes = document.querySelectorAll('.courseUser_contentListItemInfo_box');

        userCourseListItems.forEach((userCourseListItem, number)=>{
            userCourseMoreBottons[number].addEventListener('click', function(event){
                userCourseProfiles[number].classList.add('hidden');
                userCourseRoutes[number].classList.remove('invisible');
            })

            userCourseRoutes[number].addEventListener('mouseleave', function(event){
                userCourseProfiles[number].classList.remove('hidden');
                userCourseRoutes[number].classList.add('invisible');
            })
        })
    })
    const totalPages = Math.ceil(userCourseFilteredDataArray.length / itemsPerPage);
    createPageButtons(userCourseFilteredDataArray, totalPages);
}

데이터가 렌더링되는 태그의 내부를 초기화한다.

grid.innerHTML='';

기존에 선언한 paginateData 함수를 통해 인수로 받은 필터링 된 데이터 userCourseFilteredDataArray, currentPage(현재 페이지 인덱스), itemsPerPage(한 페이지 당 아이템 개수)를 입력하여 해당 카테고리, 기간에서 현재 페이지의 데이터를 가져온다.

const slicedData = paginateData(userCourseFilteredDataArray, currentPage, itemsPerPage);

그 이후 나눈 slicedData를 forEach 문으로 순회하여 데이터 별로 하나의 여행 코스 요소를 만든다.

slicedData.forEach((userCourseData, ind)=>{
    // 받아온 데이터를 이용하여 추천 여행 컨텐츠 하나를 생성하는 코드
})

이후 받아온 데이터를 이용하여 추천 여행 컨텐츠 하나를 생성하는 코드를 살펴보면

  • li 태그를 생성하여 더보기 버튼을 누르기 전 화면에 표시되는 영역을 만든다.
const newLi = document.createElement('li');
newLi.className='courseUser_contentListItem';
newLi.insertAdjacentHTML('afterbegin', `<img class="courseUser_contentListItemBackgroundImg" src="${userCourseData.Thumbnail}">
        <div class="courseUser_contentListItemCover_box">
            <a href="../course/courseContent.html">
                <strong class="courseUser_contentListItemCoverTitle">${userCourseData.Title}</strong>
                <p class="courseUser_contentListItemCoverRegion">
                    지역 :
                    <span>${userCourseData.지역}</span>
                </p>
                <p class="courseUser_contentListItemCoverDistance">
                    총거리 :
                    <span>${userCourseData.총거리}</span>
                </p>
            </a>
            <div class="courseUser_contentListItemCoverUser_box">
                <img class="courseUser_contentListItemCoverUserImg" src="${userCourseData.Avatar}" alt="">
                <span class="courseUser_contentListItemCoverUserName">${userCourseData.userInfo}</span>
            </div>
            <div class="courseUser_contentListItemCoverGocourse_box">
                <a class="courseUser_contentListItemCoverGocourseLink" href="#none">코스 더보기</a>
            </div>
        </div>`);
  • div 태그를 생성하여 더보기를 클릭했을 때 보여지는 영역을 만든다.
const newDiv = document.createElement('div');
const newUl = document.createElement('ul');
newDiv.classList.add('courseUser_contentListItemInfo_box');
newDiv.classList.add('invisible');
newUl.className='courseUser_contentListItemInfoList';

for (let i = 0; userCourseData[`area_course${i}`]; i++) {
    if (i == 0) {
        newUl.insertAdjacentHTML('beforeend', `<li class="courseUser_contentListItemInfoListItem">
                                <a class="courseUser_contentListItemInfoListItemLink" href="#none">${userCourseData.area_course}</a>
                            </li>`);
    } else {
        newUl.insertAdjacentHTML('beforeend', `<li class="courseUser_contentListItemInfoListItem">
                                <a class="courseUser_contentListItemInfoListItemLink" href="#none">${userCourseData[`area_course${i}`]}</a>
                            </li>`);
    }
}

newDiv.insertAdjacentElement('afterbegin', newUl);
newLi.insertAdjacentElement('beforeend', newDiv);
grid.appendChild(newLi);

더보기 영역의 컨테이너 역할을 하는 newDiv에 리스트 태그인 newUl을 삽입하고, for 문을 통해 useCourseData의 하위 데이터 개수만큼 (area_course의 i 번째 프로퍼티가 존재하는 만큼) li 태그를 넣어준다.

  • 새로 생성된 요소를 다시 masonary 라이브러리를 적용시키기 위해 다시 초기화 시키기.
var msnry = new Masonry(grid, {
    itemSelector: '.courseUser_contentListItem',
    columnWidth: 298
})
  • 더보기 버튼, 더보기 버튼을 클릭시 나타나는 요소에서 마우스가 빠져나올 경우 다시 감춰지도록 이벤트를 부여한다.
userCourseListItems.forEach((userCourseListItem, number)=>{
    userCourseMoreBottons[number].addEventListener('click', function(event){
        userCourseProfiles[number].classList.add('hidden');
        userCourseRoutes[number].classList.remove('invisible');
    })

    userCourseRoutes[number].addEventListener('mouseleave', function(event){
        userCourseProfiles[number].classList.remove('hidden');
        userCourseRoutes[number].classList.add('invisible');
    })
})
  • 총 페이지 수를 구한 후, 페이지 버튼을 랜더링한다.
const totalPages = Math.ceil(userCourseFilteredDataArray.length / itemsPerPage);
createPageButtons(userCourseFilteredDataArray, totalPages);

5) 카테고리 버튼을 클릭했을 때 해당 카테고리에 해당하는 데이터가 필터되게 이벤트 부여하기

userCourseCategoryButtons.forEach((userCourseCategoryButton, index)=>{
    userCourseCategoryButton.addEventListener('click', async function(event){
        loadingPage.classList.remove('hidden');
        userCourseCategoryButtons.forEach((categoryButton, buttonIndex)=>{
            if(buttonIndex === index){
                categoryButton.classList.add('courseUser_filterListItemLink--on');
            } else{
                categoryButton.classList.remove('courseUser_filterListItemLink--on');
            }
        })
        userCourseDurationButtons.forEach((durationButton, buttonIndex)=>{
            durationButton.classList.remove('courseUser_filterBarListItemLink--on');
        })
        userCourseDurationButtons[0].classList.add('courseUser_filterBarListItemLink--on');
        currentCategory=index;
        event.preventDefault();
        grid.innerHTML='';
        currentPage = 1;
        const dataCategory = userCourseCategoryButton.getAttribute('data-category');
        await getFetchedData();
        filteredData = dataArray.filter(data => data.category === dataCategory);
        const totalPages = Math.ceil(filteredData.length / itemsPerPage);
        createPageButtons(totalPages);
        renderHTML(filteredData);
        loadingPage.classList.add('hidden');
    })
})

버튼이 클릭됐을 때 해당 번째 버튼의 클래스를 강조하기 위해 변경하도록 이벤트를 부여한다.

userCourseCategoryButtons.forEach((categoryButton, buttonIndex)=>{
    if(buttonIndex === index){
        categoryButton.classList.add('courseUser_filterListItemLink--on');
    } else{
        categoryButton.classList.remove('courseUser_filterListItemLink--on');
    }
})

카테고리 버튼이 클릭됐을 때 여행 기간 필터 버튼에 부여된 강조를 위한 클래스를 초기화한다.

userCourseDurationButtons.forEach((durationButton, buttonIndex)=>{
    durationButton.classList.remove('courseUser_filterBarListItemLink--on');
})

카테고리 필터가 클릭됐을 경우 여행 기간 필터 중에 가장 첫 필터가 강조되게 클래스를 부여한다.

userCourseDurationButtons[0].classList.add('courseUser_filterBarListItemLink--on');

선택된 n 번째 카테고리 필터의 인덱스를 기존에 선언한 currentCategory에 할당한다.

currentCategory=index;

카테고리 필터가 클릭 됐을 때 여행 코스 컨텐츠들을 담는 컨테이너의 하위 노드들을 초기화하고, 현재 페이지를 1로 초기화한다.

grid.innerHTML='';
currentPage = 1;

이후 해당 버튼에서 data-category 어트리뷰트의 값을 가져온다.

const dataCategory = userCourseCategoryButton.getAttribute('data-category');

그 다음 전체 데이터를 불러오고, 가져온 data-category 값과 category 프로퍼티가 일치하는 값만 filter 메서드를 사용하여 따로 빼낸다.

await getFetchedData();
filteredData = dataArray.filter(data => data.category === dataCategory);

이후 필터된 데이터의 총 길이(데이터의 개수)를 페이지 당 아이템 개수로 나눈 값을 총 페이지로 계산하고, createPageButtons 함수에 인수로 전달한다.

const totalPages = Math.ceil(filteredData.length / itemsPerPage);
createPageButtons(totalPages);

이후 필터된 데이터를 렌더링한다.

renderHTML(filteredData);

6) 여행 기간 버튼을 클릭했을 때 해당 여행 기간에 해당하는 데이터가 필터되게 이벤트 부여하기

userCourseDurationButtons.forEach((userCourseDurationButton, index)=>{
    userCourseDurationButton.addEventListener('click', function(){
        userCourseDurationButtons.forEach((durationButton, buttonIndex)=>{
            if(buttonIndex === index){
                durationButton.classList.add('courseUser_filterBarListItemLink--on');
            } else{
                durationButton.classList.remove('courseUser_filterBarListItemLink--on');
            }
        })
        currentDuration=index;
        grid.innerHTML='';
        if(currentCategory==0){
            switch (currentDuration){
                case 0:
                    currentPage=1;
                    renderHTML(filteredData);
                    break;

                case 1:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
                
                case 2:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;

                case 3:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
            }
        } else if(currentCategory == 1){
            switch (currentDuration){
                case 0:
                    currentPage=1;
                    renderHTML(filteredData);
                    break;

                case 1:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
                
                case 2:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;

                case 3:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
            }
        } else if(currentCategory == 2){
            switch (currentDuration){
                case 0:
                    currentPage=1;
                    renderHTML(filteredData);
                    break;

                case 1:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
                
                case 2:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;

                case 3:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
            }
        } else if(currentCategory == 3){
            switch (currentDuration){
                case 0:
                    currentPage=1;
                    renderHTML(filteredData);
                    break;

                case 1:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
                
                case 2:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;

                case 3:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
            }
        } else if(currentCategory == 4){
            switch (currentDuration){
                case 0:
                    currentPage=1;
                    renderHTML(filteredData);
                    break;

                case 1:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
                
                case 2:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;

                case 3:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
            }
        } else if(currentCategory == 5){
            switch (currentDuration){
                case 0:
                    currentPage=1;
                    renderHTML(filteredData);
                    break;

                case 1:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
                
                case 2:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;

                case 3:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
            }
        } else if(currentCategory == 6){
            switch (currentDuration){
                case 0:
                    currentPage=1;
                    renderHTML(filteredData);
                    break;

                case 1:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
                
                case 2:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;

                case 3:
                    currentPage=1;
                    durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
                    renderHTML(durationFilteredData);
                    break;
            }
        }
    })
})

모든 여행 기간 필터 버튼들을 forEach 메서드로 순회한 다음에 각각의 버튼들에 이벤트를 부여한다.

userCourseDurationButtons.forEach((userCourseDurationButton, index)=>{}

이전 카테고리 필터 버튼과의 차이점은 카테고리가 클릭된 이후에 여행 기간을 필터링 한다는 점이다.

currentDuration=index;

index 번째 필터 버튼을 클릭하면 currentDuration 변수에 해당 index 인수를 대입한다.

if(currentCategory==0){
    switch (currentDuration){
        case 0:
            currentPage=1;
            renderHTML(filteredData);
            break;

        case 1:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
        
        case 2:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;

        case 3:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
    }
} else if(currentCategory == 1){
    switch (currentDuration){
        case 0:
            currentPage=1;
            renderHTML(filteredData);
            break;

        case 1:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
        
        case 2:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;

        case 3:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
    }
} else if(currentCategory == 2){
    switch (currentDuration){
        case 0:
            currentPage=1;
            renderHTML(filteredData);
            break;

        case 1:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
        
        case 2:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;

        case 3:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
    }
} else if(currentCategory == 3){
    switch (currentDuration){
        case 0:
            currentPage=1;
            renderHTML(filteredData);
            break;

        case 1:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
        
        case 2:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;

        case 3:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
    }
} else if(currentCategory == 4){
    switch (currentDuration){
        case 0:
            currentPage=1;
            renderHTML(filteredData);
            break;

        case 1:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
        
        case 2:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;

        case 3:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
    }
} else if(currentCategory == 5){
    switch (currentDuration){
        case 0:
            currentPage=1;
            renderHTML(filteredData);
            break;

        case 1:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
        
        case 2:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;

        case 3:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
    }
} else if(currentCategory == 6){
    switch (currentDuration){
        case 0:
            currentPage=1;
            renderHTML(filteredData);
            break;

        case 1:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
        
        case 2:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;

        case 3:
            currentPage=1;
            durationFilteredData = filteredData.filter(object => object.duration == currentDuration);
            renderHTML(durationFilteredData);
            break;
    }
}

이후 currentCatogory 값에 따라 (이미 선택된 카테고리 필터에 따라) 여행 기간 필터가 적용되도록 switch 문을 사용하여 durationFilteredData를 반환한다.

전체 기간을 나타내는 0번째 필터에는 그냥 기존의 filteredData를 그대로 renderHTML의 인수에 집어넣고 사용자가 1박 2일 같은 특정 구간을 클릭했을 때는 이미 카테고리로 필터된 filteredData를 다시 duration 프로퍼티의 값이 필터의 값과 일치하는 값들로 한 번 더 필터해준다.

4. 배운 내용

4-1. slice 메서드

출처 : 자바스크립트 딥 다이브

Array.prototype.slice 메서드는 전달된 범위의 요소들을 복사하여 배열로 반환한다. 원본 배열은 변경되지 않는다.

slice 메서드는 두 가지 매개변수를 갖으며 첫 매개변수는 복사를 시작할 인덱스, 두 번째 매개변수는 복사를 종료할 인덱스를 의미한다. 이 인덱스에 해당하는 요소는 복사되지 않는다.

따라서 start 이상 end 미만의 인덱스를 복사하는 개념이다.

만약에 인수를 하나만 준다면 start만 적용돼서 해당 인덱스로 시작하는 배열이 잘라져 복사된다.

ex) slice(1)

1번째 요소 부터 끝까지 잘라져서 복사된다.

만약 음수를 인수로 넣으면 뒤에서 부터 복사된다.

ex) slice(-1)

끝에서 부터 요소를 한 개 복사하여 반환한다.

ex) slice(-2)

끝에서부터 요소를 두 개 복사하여 반환한다.

4-2. Math.floor 메서드

출처 : 자바스크립트 딥 다이브

인수로 전달된 숫자의 소수점 이하를 내림한 정수를 반환한다. 내림 개념이다.

반대로 올림을 하고 싶을 떄는 Math.ceil 메서드를 사용한다.

4-3. forEach 배열 고차함수

출처 : 자바스크립트 딥 다이브

우선 고차함수란 함수를 인자로 전달받거나 함수를 결과로 반환하는 함수를 말한다. 받은 함수를 필요한 시점에 호출하거나 클로저를 생성하여 반환한다.

forEach 메서드는 for 문 대신 사용할 수 있다. 배열을 순회하며 배열의 각 요소에 대하여 인자로 주어진 콜백함수를 실행한다. 반환값은 undefined이다.

forEach 메서드는 원본 배열을 변경하지 않지만, 콜백 함수는 원본 배열을 변경할 수는 있다.

4-4. data 어트리뷰트와 dataset 프로퍼티

출처 : 자바스크립트 딥 다이브

HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있게 해주는 역할이다.

data 어트리뷰트는 data- 접두사 다음에 임의의 이름을 붙여 사용한다.

data 어트리뷰트 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있다. 혹은 getAttribute 메서드를 통해 취득 가능하다.

4-5. filter 배열 고차 함수

출처 : 자바스크립트 딥 다이브

자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출한다. 그리고 콜백 함수의 반환값이 true인 요소로만 구성된 새로운 배열을 반환한다. 이때 원본 배열은 변화시키지 않는다.

profile
프론트엔드를 희망했었던 화학공학과 취준생

0개의 댓글