튜토리얼 Tutorials/티스토리 팁 tistory tips

티스토리(tistory) 블로그에 위키백과처럼 목차, 차례가 나오도록 만들기

Tap to restart 2020. 10. 4. 22:00

티스토리 블로그 글도 위키백과처럼 목차가 쭉 나오면 좋겠다는 생각이 들었다. 그렇게 되면 원하는 부분에 바로 가서 볼 수 있으니까.

 

위키백과 목차 예

 

먼저 찾아보기

내가 만들어도 좋지만, 누군가 이미 만들지 않았을까. github에서 찾아보니 있었다!

 

Tistory TOC(Table Of Contents) by wbluke박우빈님

 

 

 

 

아쉬운 점

아래 예처럼 화면이 클 때 왼쪽 편에 뜨는 형태였다.

 

모바일 화면에서도 차례가 나오면 좋겠다는 생각이 들었다. 모바일 화면에서는 글 시작할 때 뜨고, 큰 화면에서는 글 시작할 때 뜬 목차는 사라지고, 왼쪽 편에 뜨고.

 

개선 결과

아래 예처럼 화면을 작게 하면 글 안에 차례가 나타난다.

화면을 크게 하면 글 안의 차례는 사라지고, 왼쪽 편에 차례가 나타난다.

 

 

소스 파일

Tistory TOC(Table Of Contents) by wbluke님 코드를 바탕으로 약간 수정했다.

toc.css
0.00MB
toc.js
0.01MB

위 파일을 내려받자.

그리고 관리자 - 스킨편집 - html편집 - 파일업로드로 들어가서 추가 버튼을 눌러서 추가하자.

추가하고 나면 아래처럼 된다.

 

 

적용 방법

현재 적용한 스킨은 odessey이다.

스킨에 따라 적용 결과가 다를 수 있다.

 

HTML 편집 화면에 들어간다.

관리자 - 스킨편집 - html편집 - HTML이다.

티스토리 HTML 편집 화면

1. head끝 지점 찾아서 css파일 추가

아래 예처럼 추가한다.

    <link rel="stylesheet" href="./images/toc.css">
</head>

head끝 지점 찾아서 css파일 추가

2. toc-elements div를 추가

<div id="container">를 찾아서 그 아랫줄에 <div id="toc-elements"></div>를 추가한다.

110~120번째 줄 쯤에 있다.

스킨마다 다른데 대체로 main 같은 이름으로 되어 있을 거다. 그 근처에 추가해보고 예상 결과와 다르다면 위치를 조금씩 달리해서 추가해보면 된다. 잘 모르겠다면 그냥 odessey스킨을 적용한 뒤에 따라하는 게 좋다.

        <div id="container">
            <div id="toc-elements"></div>

toc-elements div를 추가

 

3. toc-elements-in-article 추가

<div id="wrap_toc"><div id="toc-elements-in-article"></div></div>를 추가해야 한다.

 

<div class="area-view">를 찾자.

아래 쪽으로 좀 내려가면

<div class="article-view">

티스토리 블로그 글도 위키백과처럼 목차가 쭉 나오면 좋겠다는 생각이 들었다. 그렇게 되면 원하는 부분에 바로 가서 볼 수 있으니까.

 

위키백과 목차 예

 

먼저 찾아보기

내가 만들어도 좋지만, 누군가 이미 만들지 않았을까. github에서 찾아보니 있었다!

 

Tistory TOC(Table Of Contents) by wbluke박우빈님

 

 

 

 

아쉬운 점

아래 예처럼 화면이 클 때 왼쪽 편에 뜨는 형태였다.

 

모바일 화면에서도 차례가 나오면 좋겠다는 생각이 들었다. 모바일 화면에서는 글 시작할 때 뜨고, 큰 화면에서는 글 시작할 때 뜬 목차는 사라지고, 왼쪽 편에 뜨고.

 

개선 결과

아래 예처럼 화면을 작게 하면 글 안에 차례가 나타난다.

화면을 크게 하면 글 안의 차례는 사라지고, 왼쪽 편에 차례가 나타난다.

 

 

소스 파일

Tistory TOC(Table Of Contents) by wbluke님 코드를 바탕으로 약간 수정했다.

toc.css
0.00MB
toc.js
0.01MB

위 파일을 내려받자.

그리고 관리자 - 스킨편집 - html편집 - 파일업로드로 들어가서 추가 버튼을 눌러서 추가하자.

추가하고 나면 아래처럼 된다.

 

 

적용 방법

현재 적용한 스킨은 odessey이다.

스킨에 따라 적용 결과가 다를 수 있다.

 

HTML 편집 화면에 들어간다.

관리자 - 스킨편집 - html편집 - HTML이다.

티스토리 HTML 편집 화면

1. head끝 지점 찾아서 css파일 추가

아래 예처럼 추가한다.

    <link rel="stylesheet" href="./images/toc.css">
</head>

head끝 지점 찾아서 css파일 추가

2. toc-elements div를 추가

<div id="container">를 찾아서 그 아랫줄에 <div id="toc-elements"></div>를 추가한다.

110~120번째 줄 쯤에 있다.

스킨마다 다른데 대체로 main 같은 이름으로 되어 있을 거다. 그 근처에 추가해보고 예상 결과와 다르다면 위치를 조금씩 달리해서 추가해보면 된다. 잘 모르겠다면 그냥 odessey스킨을 적용한 뒤에 따라하는 게 좋다.

        <div id="container">
            <div id="toc-elements"></div>

toc-elements div를 추가

 

3. toc-elements-in-article 추가

<div id="wrap_toc"><div id="toc-elements-in-article"></div></div>를 추가해야 한다.

 

<div class="area-view">를 찾자.

아래 쪽으로 좀 내려가면

<div class="article-view">

</div>

가 나타난다.

이 위에 추가한다.

<div id="wrap_toc"><div id="toc-elements-in-article"></div></div>
<div class="article-view">
	
</div>

210~220번째 줄에 있다

toc-elements-in-article 추가

4. script 추가

</body>위에 <script src="./images/toc.js"></script>를 추가한다.

맨 끝에 있다.

<script src="./images/toc.js"></script>
</body>

 

 

끝이다.

이제 각 블로그 글마다 차례가 나타날 것이다.

차례가 나타나지 않는 경우는 글 작성할 때 제목1, 제목2, 제목3을 사용하지 않은 경우다.

티스토리 제목 스타일

제목 스타일을 적용해야지만 차례가 나타난다.

 

코드

toc.js

 

추가한 부분

const TOC_CARD = (function () {
    const TocCardController = function () {
        const registerHTagsOnTocCard = function () {
            tocCardService.registerTagsOnTocInArticle(levelMap);
        }

        const onresize = function () {
            const width = document.body.clientWidth;
            if(width > 1420){
                tocCardService.removeTOCInArticle();
            }else{
                tocCardService.addTOCInArticle();
            }
        }

        return {
            onresize
        };
    };
    
    const TocCardService = function () {        
        const tocElementsCardInArticle = document.querySelector('#toc-elements-in-article');
        const wrapToc = document.querySelector('#wrap_toc');
        
        const removeTOCInArticle = function(){
            if(wrapToc.childNodes.length > 0){
                wrapToc.removeChild(tocElementsCardInArticle);
            }
        }
        const addTOCInArticle = function(){
            if(wrapToc.childNodes.length === 0){
                wrapToc.appendChild(tocElementsCardInArticle);
            }
        }              

        const registerTagsOnTocInArticle = function (levelMap) {
            if(hTags.length > 0){
                const tocH = document.createElement("h2");
                tocH.append("차례");
                tocElementsCardInArticle.appendChild(tocH);
            }
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCardInArticle.appendChild(hTagItem);
            });


            const width = document.body.clientWidth;
            if(width > 1420){
                removeTOCInArticle();
            }
        }

        return {
            registerTagsOnTocInArticle,
            removeTOCInArticle,
            addTOCInArticle
        }
    };
        

    const onresize = function () {
        tocCardController.onresize();
    }

    return {
        onresize
    }
})();
        

window.onresize = function(){
    TOC_CARD.onresize();
}

 

 

전체

/*
 * Tistory TOC (Table Of Contents)
 * dev by wbluke (wbluke.com)
 * last update 2020.05.06
 * version 0.1.5
 * https://github.com/wbluke/tistory-table-of-contents
 * https://www.wbluke.com/21
 */

const CLASS_OF_MAIN_CONTENTS = '.article-view';

const CONSTANTS = (function () {
    const KEY_OF_H1 = 1;
    const KEY_OF_H2 = 2;
    const KEY_OF_H3 = 3;
    const KEY_OF_H4 = 4;

    const LEVEL_1 = 1;
    const LEVEL_2 = 2;
    const LEVEL_3 = 3;
    const LEVEL_4 = 4;

    /* 최상위 태그에 따른 레벨 Map */
    const levelsByH1 = function () {
        return new Map([[KEY_OF_H1, LEVEL_1], [KEY_OF_H2, LEVEL_2], [KEY_OF_H3, LEVEL_3], [KEY_OF_H4, LEVEL_4]])
    }

    const levelsByH2 = function () {
        return new Map([[KEY_OF_H2, LEVEL_1], [KEY_OF_H3, LEVEL_2], [KEY_OF_H4, LEVEL_3]])
    }

    const levelsByH3 = function () {
        return new Map([[KEY_OF_H3, LEVEL_1], [KEY_OF_H4, LEVEL_2]])
    }

    const levelsByH4 = function () {
        return new Map([[KEY_OF_H4, LEVEL_1]])
    }

    return {
        indexOfH1: KEY_OF_H1,
        indexOfH2: KEY_OF_H2,
        indexOfH3: KEY_OF_H3,
        indexOfH4: KEY_OF_H4,
        levelsByH1: levelsByH1(),
        levelsByH2: levelsByH2(),
        levelsByH3: levelsByH3(),
        levelsByH4: levelsByH4(),
    }
})();

const TOC_CARD = (function () {
    const TocCardController = function () {

        const tocCardService = new TocCardService();

        const initTocElementsCard = function () {
            tocCardService.initTocElementsCard();
        }

        const giveIdToHTags = function () {
            tocCardService.giveIdToHTags();
        }

        const registerHTagsOnTocCard = function () {
            const levelMap = tocCardService.getLevelsByHighestTag();

            tocCardService.registerTagsOnToc(levelMap);
            tocCardService.registerTagsOnTocInArticle(levelMap);
        }

        const init = function () {
            const existsHTags = tocCardService.checkExistenceOfHTags();
            if (existsHTags) {
                initTocElementsCard();
                giveIdToHTags();
                registerHTagsOnTocCard();
            }
        };

        const onscroll = function () {
            const tocTag = tocCardService.findCurrentHTag();

            if (tocTag) {
                tocCardService.markCurrentHTag(tocTag);
                tocCardService.scrollToMainTocTag(tocTag);
                tocCardService.detectTocCardPosition();
            }
        }

        const onresize = function () {
            const width = document.body.clientWidth;
            if(width > 1420){
                tocCardService.removeTOCInArticle();
            }else{
                tocCardService.addTOCInArticle();
            }
        }

        return {
            init,
            onscroll,
            onresize
        };
    };

    const TocCardService = function () {
        const tocElementsCard = document.querySelector('#toc-elements');
        const tocElementsCardInArticle = document.querySelector('#toc-elements-in-article');
        const wrapToc = document.querySelector('#wrap_toc');

        const removeTOCInArticle = function(){
            if(wrapToc.childNodes.length > 0){
                wrapToc.removeChild(tocElementsCardInArticle);
            }
        }
        const addTOCInArticle = function(){
            if(wrapToc.childNodes.length === 0){
                wrapToc.appendChild(tocElementsCardInArticle);
            }
        }

        const mainContents = document.querySelector(CLASS_OF_MAIN_CONTENTS);

        const hTags = (function () {
            const foundHTags = mainContents.querySelectorAll('h1, h2, h3, h4');

            /* 글 내용 밑에 있는 [...카테고리의 다른 글] h4 제거 */
            return [...foundHTags].filter(hTag =>
                !hTag.parentElement.classList.contains('another_category') &&
                !hTag.parentElement.classList.contains('tags') &&
                !hTag.parentElement.classList.contains('related-articles'));
        })();

        /* h1, h2, h3, h4 태그가 있는지 확인한다 */
        const checkExistenceOfHTags = function () {
            if (mainContents === undefined) {
                return false;
            }

            return hTags.length !== 0;
        }

        const initTocElementsCard = function () {
            tocElementsCard.classList.add('toc-app-common', 'toc-app-items', 'toc-app-basic');
        }

        /** 최상위 태그에 따른 레벨 Map 받아오기
         *
         * h1 ~ h4 태그 중 가장 높은 태그를 찾아서 그에 맞게 Level을 설정한다.
         * 예를 들어, h1 태그가 없고 h2, h3 태그만 있는 경우
         * h2가 가장 높은 태그이며, 해당 태그 h2에 LEVEL_1을 부여하고 그 다음 태그인 h3에는 LEVEL_2를 부여한다.
         *
         * 부여된 Level에 따라 적용되는 CSS가 달라진다.
         * */
        const getLevelsByHighestTag = function () {
            const levelMapByHighestTag = {
                'H1': CONSTANTS.levelsByH1,
                'H2': CONSTANTS.levelsByH2,
                'H3': CONSTANTS.levelsByH3,
            };

            return levelMapByHighestTag[findHighestHTag().tagName] || CONSTANTS.levelsByH4;
        }

        /* 최상위 태그 판별 작업 */
        const findHighestHTag = function () {
            return [...hTags].reduce((pre, cur) => {
                const tagNumOfPre = parseInt(pre.tagName[1]);
                const tagNumOfCur = parseInt(cur.tagName[1]);

                return (tagNumOfPre < tagNumOfCur) ? pre : cur;
            });
        }

        /* TOC에 태그 삽입 */
        const registerTagsOnToc = function (levelMap) {
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCard.appendChild(hTagItem);
            });
        }

        const registerTagsOnTocInArticle = function (levelMap) {
            if(hTags.length > 0){
                const tocH = document.createElement("h2");
                tocH.append("차례");
                tocElementsCardInArticle.appendChild(tocH);
            }
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCardInArticle.appendChild(hTagItem);
            });


            const width = document.body.clientWidth;
            if(width > 1420){
                removeTOCInArticle();
            }
        }

        const createTagItemByLevel = function (level = CONSTANTS.NUM_OF_H1, hTag, indexOfHTag) {
            const basicItem = createBasicItemBy(hTag, indexOfHTag);
            appendScrollEventsOn(basicItem, indexOfHTag);

            basicItem.classList.add(`toc-level-${level}`);

            return basicItem;
        }

        const createBasicItemBy = function (hTag, indexOfHTag) {
            const basicItem = document.createElement('a');

            basicItem.innerHTML += hTag.innerText;
            basicItem.id = `toc-${indexOfHTag}`;
            basicItem.classList = 'toc-common';

            return basicItem;
        }

        const generateIdOfHTag = function (indexOfHTag) {
            return 'h-tag-' + indexOfHTag;
        }

        const appendScrollEventsOn = function (basicItem, indexOfHTag) {
            const target = document.querySelector('#' + generateIdOfHTag(indexOfHTag));
            basicItem.addEventListener('click', () => window.scrollTo({
                top: target.offsetTop - 10,
                behavior: 'smooth'
            }));
        }

        const giveIdToHTags = function () {
            hTags.forEach((hTag, indexOfHTag) => {
                hTag.id = generateIdOfHTag(indexOfHTag);
            });
        }

        const findCurrentHTag = function () {
            if (hTags.length == 0) {
                return undefined;
            }

            const currentHTag = findCurrentMainHTag();
            return findTocTagCorrespondingToHTag(currentHTag);
        }

        const findCurrentMainHTag = function () {
            const headArea = document.querySelector('.area_head');

            let headAreaHeight = 0;
            if (headArea) {
                headAreaHeight = headArea.offsetHeight;
            }

            const middleHeight = window.scrollY + (window.innerHeight / 2) - headAreaHeight;

            return [...hTags].reduce((pre, cur) => {
                if (middleHeight < pre.offsetTop && middleHeight < cur.offsetTop) {
                    return pre;
                }

                if (pre.offsetTop < middleHeight && middleHeight <= cur.offsetTop) {
                    return pre;
                }

                return cur;
            });
        }

        const findTocTagCorrespondingToHTag = function (currentHTag) {
            const indexOfHTag = parseIndexOfTag(currentHTag);

            return document.querySelector(`#toc-${indexOfHTag}`);
        }

        const parseIndexOfTag = function (hTag) {
            const tokens = hTag.id.split('-');
            return parseInt(tokens[tokens.length - 1]);
        }

        const markCurrentHTag = function (tocTag) {
            removeAllClassOnTocTags('toc-active');
            tocTag.classList.add('toc-active');
            markParentHTagOf(tocTag);
        }

        const removeAllClassOnTocTags = function (className) {
            Array.prototype.slice.call(tocElementsCard.children).forEach(child => {
                child.classList.remove(className);
            });
        }

        const markParentHTagOf = function (tocTag) {
            const indexOfTocTag = parseIndexOfTag(tocTag);
            const levelOfBaseTocTag = findLevelOfTocTag(tocTag);

            removeAllClassOnTocTags('toc-parent-active');
            compareLevelAndMark(levelOfBaseTocTag, indexOfTocTag);
        }

        /**
         * 현재 active 태그의 부모 레벨 태그를 표시
         * 기준 태그(active 태그)애서 하나씩 위로 올라가면서 부모 태그를 탐색 (재귀)
         * */
        const compareLevelAndMark = function (levelOfBaseTocTag, indexOfCurrentTocTag) {
            if (levelOfBaseTocTag <= 1 || indexOfCurrentTocTag < 0) {
                return;
            }

            const currentTocTag = document.querySelector(`#toc-${indexOfCurrentTocTag}`);
            const levelOfCurrentTocTag = findLevelOfTocTag(currentTocTag);

            if (levelOfBaseTocTag <= levelOfCurrentTocTag) {
                return compareLevelAndMark(levelOfBaseTocTag, indexOfCurrentTocTag - 1);
            }

            currentTocTag.classList.add('toc-parent-active')
            compareLevelAndMark(levelOfBaseTocTag - 1, indexOfCurrentTocTag - 1);
        }

        const findLevelOfTocTag = function (tocTag) {
            const classes = tocTag.classList;
            if (classes.contains('toc-level-4')) {
                return 4;
            }

            if (classes.contains('toc-level-3')) {
                return 3;
            }

            if (classes.contains('toc-level-2')) {
                return 2;
            }

            return 1;
        }

        /**
         * TOC 항목이 너무 많아 TOC Card에 스크롤이 생길 경우,
         * 스크롤 이벤트에 따라 활성화된 TOC 태그가 보이도록 TOC Card의 스크롤도 함께 이동한다.
         */
        const scrollToMainTocTag = function (tocTag) {
            tocElementsCard.scroll({
                top: tocTag.offsetTop - (tocTag.offsetParent.offsetHeight * 0.3),
                behavior: 'smooth'
            });
        }

        const detectTocCardPosition = function () {
            const currentScrollTop = document.documentElement.scrollTop;

            const footer = document.querySelector('#mEtc');

            let footerTop = Number.MAX_SAFE_INTEGER;
            if (footer) {
                footerTop = footer.offsetTop;
            }

            const elementsCardBottom = currentScrollTop + tocElementsCard.offsetHeight;

            tocElementsCard.classList.remove('toc-app-basic', 'toc-app-bottom');

            if (elementsCardBottom >= footerTop) {
                tocElementsCard.classList.add('toc-app-bottom');
                return;
            }

            tocElementsCard.classList.add('toc-app-basic');
        }

        return {
            checkExistenceOfHTags,
            initTocElementsCard,
            getLevelsByHighestTag,
            registerTagsOnToc,
            giveIdToHTags,
            findCurrentHTag,
            markCurrentHTag,
            scrollToMainTocTag,
            detectTocCardPosition,
            registerTagsOnTocInArticle,
            removeTOCInArticle,
            addTOCInArticle
        }
    };

    const tocCardController = new TocCardController();

    const init = function () {
        tocCardController.init();
    };

    const onscroll = function () {
        tocCardController.onscroll();
    }

    const onresize = function () {
        tocCardController.onresize();
    }

    return {
        init,
        onscroll,
        onresize
    }
})();

TOC_CARD.init();

/**
 * scroll 시 현재 내용의 위치를 스크롤 이벤트를 통해 TOC에 표시해주기
 */
window.onscroll = function () {
    TOC_CARD.onscroll();
}

window.onresize = function(){
    TOC_CARD.onresize();
}

 

toc.css

추가한 부분

#wrap_toc{
    padding-bottom: 1.3rem;
}

 

 

전체

/* custom card style */

.toc-app-common {
    display: inline-block;
    padding: 1rem;
}

.toc-app-basic {
    position: fixed;
}

.toc-app-bottom {
    position: absolute;
}

/* custom item style */

.toc-app-items {
    height: 80vh;
    width: 15.3rem;
    overflow-y: scroll;
    -ms-overflow-style: none;
}

.toc-app-items::-webkit-scrollbar {
    display: none !important;
}

.toc-common {
    display: block;
    cursor: pointer;
    color: grey;
}

.toc-level-1 {
    font-size: 1.25rem;
    margin-top: 0.5rem;
    font-weight: bold;
}

.toc-level-2 {
    font-size: 1.1rem;
    margin-top: 0.3rem;
    margin-left: 0.8rem;
}

.toc-level-3 {
    font-size: 1.0rem;
    margin-top: 0.3rem;
    margin-left: 1.5rem;
}

.toc-level-4 {
    font-size: 0.9rem;
    margin-top: 0.3rem;
    margin-left: 2.2rem;
}

.toc-active, .toc-common:hover {
    color: #000;
}

.toc-parent-active {
    color: #333;
}

@media (max-width: 90rem) {
    .toc-app-common {
        visibility: hidden;
    }
}

#wrap_toc{
    padding-bottom: 1.3rem;
}

</div>

가 나타난다.

이 위에 추가한다.

<div id="wrap_toc"><div id="toc-elements-in-article"></div></div>
<div class="article-view">
	
            



            

티스토리 블로그 글도 위키백과처럼 목차가 쭉 나오면 좋겠다는 생각이 들었다. 그렇게 되면 원하는 부분에 바로 가서 볼 수 있으니까.

 

위키백과 목차 예

 

먼저 찾아보기

내가 만들어도 좋지만, 누군가 이미 만들지 않았을까. github에서 찾아보니 있었다!

 

Tistory TOC(Table Of Contents) by wbluke박우빈님

 

 

 

 

아쉬운 점

아래 예처럼 화면이 클 때 왼쪽 편에 뜨는 형태였다.

 

모바일 화면에서도 차례가 나오면 좋겠다는 생각이 들었다. 모바일 화면에서는 글 시작할 때 뜨고, 큰 화면에서는 글 시작할 때 뜬 목차는 사라지고, 왼쪽 편에 뜨고.

 

개선 결과

아래 예처럼 화면을 작게 하면 글 안에 차례가 나타난다.

화면을 크게 하면 글 안의 차례는 사라지고, 왼쪽 편에 차례가 나타난다.

 

 

소스 파일

Tistory TOC(Table Of Contents) by wbluke님 코드를 바탕으로 약간 수정했다.

toc.css
0.00MB
toc.js
0.01MB

위 파일을 내려받자.

그리고 관리자 - 스킨편집 - html편집 - 파일업로드로 들어가서 추가 버튼을 눌러서 추가하자.

추가하고 나면 아래처럼 된다.

 

 

적용 방법

현재 적용한 스킨은 odessey이다.

스킨에 따라 적용 결과가 다를 수 있다.

 

HTML 편집 화면에 들어간다.

관리자 - 스킨편집 - html편집 - HTML이다.

티스토리 HTML 편집 화면

1. head끝 지점 찾아서 css파일 추가

아래 예처럼 추가한다.

    <link rel="stylesheet" href="./images/toc.css">
</head>

head끝 지점 찾아서 css파일 추가

2. toc-elements div를 추가

<div id="container">를 찾아서 그 아랫줄에 <div id="toc-elements"></div>를 추가한다.

110~120번째 줄 쯤에 있다.

스킨마다 다른데 대체로 main 같은 이름으로 되어 있을 거다. 그 근처에 추가해보고 예상 결과와 다르다면 위치를 조금씩 달리해서 추가해보면 된다. 잘 모르겠다면 그냥 odessey스킨을 적용한 뒤에 따라하는 게 좋다.

        <div id="container">
            <div id="toc-elements"></div>

toc-elements div를 추가

 

3. toc-elements-in-article 추가

<div id="wrap_toc"><div id="toc-elements-in-article"></div></div>를 추가해야 한다.

 

<div class="area-view">를 찾자.

아래 쪽으로 좀 내려가면

<div class="article-view">

</div>

가 나타난다.

이 위에 추가한다.

<div id="wrap_toc"><div id="toc-elements-in-article"></div></div>
<div class="article-view">
	
</div>

210~220번째 줄에 있다

toc-elements-in-article 추가

4. script 추가

</body>위에 <script src="./images/toc.js"></script>를 추가한다.

맨 끝에 있다.

<script src="./images/toc.js"></script>
</body>

 

 

끝이다.

이제 각 블로그 글마다 차례가 나타날 것이다.

차례가 나타나지 않는 경우는 글 작성할 때 제목1, 제목2, 제목3을 사용하지 않은 경우다.

티스토리 제목 스타일

제목 스타일을 적용해야지만 차례가 나타난다.

 

코드

toc.js

 

추가한 부분

const TOC_CARD = (function () {
    const TocCardController = function () {
        const registerHTagsOnTocCard = function () {
            tocCardService.registerTagsOnTocInArticle(levelMap);
        }

        const onresize = function () {
            const width = document.body.clientWidth;
            if(width > 1420){
                tocCardService.removeTOCInArticle();
            }else{
                tocCardService.addTOCInArticle();
            }
        }

        return {
            onresize
        };
    };
    
    const TocCardService = function () {        
        const tocElementsCardInArticle = document.querySelector('#toc-elements-in-article');
        const wrapToc = document.querySelector('#wrap_toc');
        
        const removeTOCInArticle = function(){
            if(wrapToc.childNodes.length > 0){
                wrapToc.removeChild(tocElementsCardInArticle);
            }
        }
        const addTOCInArticle = function(){
            if(wrapToc.childNodes.length === 0){
                wrapToc.appendChild(tocElementsCardInArticle);
            }
        }              

        const registerTagsOnTocInArticle = function (levelMap) {
            if(hTags.length > 0){
                const tocH = document.createElement("h2");
                tocH.append("차례");
                tocElementsCardInArticle.appendChild(tocH);
            }
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCardInArticle.appendChild(hTagItem);
            });


            const width = document.body.clientWidth;
            if(width > 1420){
                removeTOCInArticle();
            }
        }

        return {
            registerTagsOnTocInArticle,
            removeTOCInArticle,
            addTOCInArticle
        }
    };
        

    const onresize = function () {
        tocCardController.onresize();
    }

    return {
        onresize
    }
})();
        

window.onresize = function(){
    TOC_CARD.onresize();
}

 

 

전체

/*
 * Tistory TOC (Table Of Contents)
 * dev by wbluke (wbluke.com)
 * last update 2020.05.06
 * version 0.1.5
 * https://github.com/wbluke/tistory-table-of-contents
 * https://www.wbluke.com/21
 */

const CLASS_OF_MAIN_CONTENTS = '.article-view';

const CONSTANTS = (function () {
    const KEY_OF_H1 = 1;
    const KEY_OF_H2 = 2;
    const KEY_OF_H3 = 3;
    const KEY_OF_H4 = 4;

    const LEVEL_1 = 1;
    const LEVEL_2 = 2;
    const LEVEL_3 = 3;
    const LEVEL_4 = 4;

    /* 최상위 태그에 따른 레벨 Map */
    const levelsByH1 = function () {
        return new Map([[KEY_OF_H1, LEVEL_1], [KEY_OF_H2, LEVEL_2], [KEY_OF_H3, LEVEL_3], [KEY_OF_H4, LEVEL_4]])
    }

    const levelsByH2 = function () {
        return new Map([[KEY_OF_H2, LEVEL_1], [KEY_OF_H3, LEVEL_2], [KEY_OF_H4, LEVEL_3]])
    }

    const levelsByH3 = function () {
        return new Map([[KEY_OF_H3, LEVEL_1], [KEY_OF_H4, LEVEL_2]])
    }

    const levelsByH4 = function () {
        return new Map([[KEY_OF_H4, LEVEL_1]])
    }

    return {
        indexOfH1: KEY_OF_H1,
        indexOfH2: KEY_OF_H2,
        indexOfH3: KEY_OF_H3,
        indexOfH4: KEY_OF_H4,
        levelsByH1: levelsByH1(),
        levelsByH2: levelsByH2(),
        levelsByH3: levelsByH3(),
        levelsByH4: levelsByH4(),
    }
})();

const TOC_CARD = (function () {
    const TocCardController = function () {

        const tocCardService = new TocCardService();

        const initTocElementsCard = function () {
            tocCardService.initTocElementsCard();
        }

        const giveIdToHTags = function () {
            tocCardService.giveIdToHTags();
        }

        const registerHTagsOnTocCard = function () {
            const levelMap = tocCardService.getLevelsByHighestTag();

            tocCardService.registerTagsOnToc(levelMap);
            tocCardService.registerTagsOnTocInArticle(levelMap);
        }

        const init = function () {
            const existsHTags = tocCardService.checkExistenceOfHTags();
            if (existsHTags) {
                initTocElementsCard();
                giveIdToHTags();
                registerHTagsOnTocCard();
            }
        };

        const onscroll = function () {
            const tocTag = tocCardService.findCurrentHTag();

            if (tocTag) {
                tocCardService.markCurrentHTag(tocTag);
                tocCardService.scrollToMainTocTag(tocTag);
                tocCardService.detectTocCardPosition();
            }
        }

        const onresize = function () {
            const width = document.body.clientWidth;
            if(width > 1420){
                tocCardService.removeTOCInArticle();
            }else{
                tocCardService.addTOCInArticle();
            }
        }

        return {
            init,
            onscroll,
            onresize
        };
    };

    const TocCardService = function () {
        const tocElementsCard = document.querySelector('#toc-elements');
        const tocElementsCardInArticle = document.querySelector('#toc-elements-in-article');
        const wrapToc = document.querySelector('#wrap_toc');

        const removeTOCInArticle = function(){
            if(wrapToc.childNodes.length > 0){
                wrapToc.removeChild(tocElementsCardInArticle);
            }
        }
        const addTOCInArticle = function(){
            if(wrapToc.childNodes.length === 0){
                wrapToc.appendChild(tocElementsCardInArticle);
            }
        }

        const mainContents = document.querySelector(CLASS_OF_MAIN_CONTENTS);

        const hTags = (function () {
            const foundHTags = mainContents.querySelectorAll('h1, h2, h3, h4');

            /* 글 내용 밑에 있는 [...카테고리의 다른 글] h4 제거 */
            return [...foundHTags].filter(hTag =>
                !hTag.parentElement.classList.contains('another_category') &&
                !hTag.parentElement.classList.contains('tags') &&
                !hTag.parentElement.classList.contains('related-articles'));
        })();

        /* h1, h2, h3, h4 태그가 있는지 확인한다 */
        const checkExistenceOfHTags = function () {
            if (mainContents === undefined) {
                return false;
            }

            return hTags.length !== 0;
        }

        const initTocElementsCard = function () {
            tocElementsCard.classList.add('toc-app-common', 'toc-app-items', 'toc-app-basic');
        }

        /** 최상위 태그에 따른 레벨 Map 받아오기
         *
         * h1 ~ h4 태그 중 가장 높은 태그를 찾아서 그에 맞게 Level을 설정한다.
         * 예를 들어, h1 태그가 없고 h2, h3 태그만 있는 경우
         * h2가 가장 높은 태그이며, 해당 태그 h2에 LEVEL_1을 부여하고 그 다음 태그인 h3에는 LEVEL_2를 부여한다.
         *
         * 부여된 Level에 따라 적용되는 CSS가 달라진다.
         * */
        const getLevelsByHighestTag = function () {
            const levelMapByHighestTag = {
                'H1': CONSTANTS.levelsByH1,
                'H2': CONSTANTS.levelsByH2,
                'H3': CONSTANTS.levelsByH3,
            };

            return levelMapByHighestTag[findHighestHTag().tagName] || CONSTANTS.levelsByH4;
        }

        /* 최상위 태그 판별 작업 */
        const findHighestHTag = function () {
            return [...hTags].reduce((pre, cur) => {
                const tagNumOfPre = parseInt(pre.tagName[1]);
                const tagNumOfCur = parseInt(cur.tagName[1]);

                return (tagNumOfPre < tagNumOfCur) ? pre : cur;
            });
        }

        /* TOC에 태그 삽입 */
        const registerTagsOnToc = function (levelMap) {
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCard.appendChild(hTagItem);
            });
        }

        const registerTagsOnTocInArticle = function (levelMap) {
            if(hTags.length > 0){
                const tocH = document.createElement("h2");
                tocH.append("차례");
                tocElementsCardInArticle.appendChild(tocH);
            }
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCardInArticle.appendChild(hTagItem);
            });


            const width = document.body.clientWidth;
            if(width > 1420){
                removeTOCInArticle();
            }
        }

        const createTagItemByLevel = function (level = CONSTANTS.NUM_OF_H1, hTag, indexOfHTag) {
            const basicItem = createBasicItemBy(hTag, indexOfHTag);
            appendScrollEventsOn(basicItem, indexOfHTag);

            basicItem.classList.add(`toc-level-${level}`);

            return basicItem;
        }

        const createBasicItemBy = function (hTag, indexOfHTag) {
            const basicItem = document.createElement('a');

            basicItem.innerHTML += hTag.innerText;
            basicItem.id = `toc-${indexOfHTag}`;
            basicItem.classList = 'toc-common';

            return basicItem;
        }

        const generateIdOfHTag = function (indexOfHTag) {
            return 'h-tag-' + indexOfHTag;
        }

        const appendScrollEventsOn = function (basicItem, indexOfHTag) {
            const target = document.querySelector('#' + generateIdOfHTag(indexOfHTag));
            basicItem.addEventListener('click', () => window.scrollTo({
                top: target.offsetTop - 10,
                behavior: 'smooth'
            }));
        }

        const giveIdToHTags = function () {
            hTags.forEach((hTag, indexOfHTag) => {
                hTag.id = generateIdOfHTag(indexOfHTag);
            });
        }

        const findCurrentHTag = function () {
            if (hTags.length == 0) {
                return undefined;
            }

            const currentHTag = findCurrentMainHTag();
            return findTocTagCorrespondingToHTag(currentHTag);
        }

        const findCurrentMainHTag = function () {
            const headArea = document.querySelector('.area_head');

            let headAreaHeight = 0;
            if (headArea) {
                headAreaHeight = headArea.offsetHeight;
            }

            const middleHeight = window.scrollY + (window.innerHeight / 2) - headAreaHeight;

            return [...hTags].reduce((pre, cur) => {
                if (middleHeight < pre.offsetTop && middleHeight < cur.offsetTop) {
                    return pre;
                }

                if (pre.offsetTop < middleHeight && middleHeight <= cur.offsetTop) {
                    return pre;
                }

                return cur;
            });
        }

        const findTocTagCorrespondingToHTag = function (currentHTag) {
            const indexOfHTag = parseIndexOfTag(currentHTag);

            return document.querySelector(`#toc-${indexOfHTag}`);
        }

        const parseIndexOfTag = function (hTag) {
            const tokens = hTag.id.split('-');
            return parseInt(tokens[tokens.length - 1]);
        }

        const markCurrentHTag = function (tocTag) {
            removeAllClassOnTocTags('toc-active');
            tocTag.classList.add('toc-active');
            markParentHTagOf(tocTag);
        }

        const removeAllClassOnTocTags = function (className) {
            Array.prototype.slice.call(tocElementsCard.children).forEach(child => {
                child.classList.remove(className);
            });
        }

        const markParentHTagOf = function (tocTag) {
            const indexOfTocTag = parseIndexOfTag(tocTag);
            const levelOfBaseTocTag = findLevelOfTocTag(tocTag);

            removeAllClassOnTocTags('toc-parent-active');
            compareLevelAndMark(levelOfBaseTocTag, indexOfTocTag);
        }

        /**
         * 현재 active 태그의 부모 레벨 태그를 표시
         * 기준 태그(active 태그)애서 하나씩 위로 올라가면서 부모 태그를 탐색 (재귀)
         * */
        const compareLevelAndMark = function (levelOfBaseTocTag, indexOfCurrentTocTag) {
            if (levelOfBaseTocTag <= 1 || indexOfCurrentTocTag < 0) {
                return;
            }

            const currentTocTag = document.querySelector(`#toc-${indexOfCurrentTocTag}`);
            const levelOfCurrentTocTag = findLevelOfTocTag(currentTocTag);

            if (levelOfBaseTocTag <= levelOfCurrentTocTag) {
                return compareLevelAndMark(levelOfBaseTocTag, indexOfCurrentTocTag - 1);
            }

            currentTocTag.classList.add('toc-parent-active')
            compareLevelAndMark(levelOfBaseTocTag - 1, indexOfCurrentTocTag - 1);
        }

        const findLevelOfTocTag = function (tocTag) {
            const classes = tocTag.classList;
            if (classes.contains('toc-level-4')) {
                return 4;
            }

            if (classes.contains('toc-level-3')) {
                return 3;
            }

            if (classes.contains('toc-level-2')) {
                return 2;
            }

            return 1;
        }

        /**
         * TOC 항목이 너무 많아 TOC Card에 스크롤이 생길 경우,
         * 스크롤 이벤트에 따라 활성화된 TOC 태그가 보이도록 TOC Card의 스크롤도 함께 이동한다.
         */
        const scrollToMainTocTag = function (tocTag) {
            tocElementsCard.scroll({
                top: tocTag.offsetTop - (tocTag.offsetParent.offsetHeight * 0.3),
                behavior: 'smooth'
            });
        }

        const detectTocCardPosition = function () {
            const currentScrollTop = document.documentElement.scrollTop;

            const footer = document.querySelector('#mEtc');

            let footerTop = Number.MAX_SAFE_INTEGER;
            if (footer) {
                footerTop = footer.offsetTop;
            }

            const elementsCardBottom = currentScrollTop + tocElementsCard.offsetHeight;

            tocElementsCard.classList.remove('toc-app-basic', 'toc-app-bottom');

            if (elementsCardBottom >= footerTop) {
                tocElementsCard.classList.add('toc-app-bottom');
                return;
            }

            tocElementsCard.classList.add('toc-app-basic');
        }

        return {
            checkExistenceOfHTags,
            initTocElementsCard,
            getLevelsByHighestTag,
            registerTagsOnToc,
            giveIdToHTags,
            findCurrentHTag,
            markCurrentHTag,
            scrollToMainTocTag,
            detectTocCardPosition,
            registerTagsOnTocInArticle,
            removeTOCInArticle,
            addTOCInArticle
        }
    };

    const tocCardController = new TocCardController();

    const init = function () {
        tocCardController.init();
    };

    const onscroll = function () {
        tocCardController.onscroll();
    }

    const onresize = function () {
        tocCardController.onresize();
    }

    return {
        init,
        onscroll,
        onresize
    }
})();

TOC_CARD.init();

/**
 * scroll 시 현재 내용의 위치를 스크롤 이벤트를 통해 TOC에 표시해주기
 */
window.onscroll = function () {
    TOC_CARD.onscroll();
}

window.onresize = function(){
    TOC_CARD.onresize();
}

 

toc.css

추가한 부분

#wrap_toc{
    padding-bottom: 1.3rem;
}

 

 

전체

/* custom card style */

.toc-app-common {
    display: inline-block;
    padding: 1rem;
}

.toc-app-basic {
    position: fixed;
}

.toc-app-bottom {
    position: absolute;
}

/* custom item style */

.toc-app-items {
    height: 80vh;
    width: 15.3rem;
    overflow-y: scroll;
    -ms-overflow-style: none;
}

.toc-app-items::-webkit-scrollbar {
    display: none !important;
}

.toc-common {
    display: block;
    cursor: pointer;
    color: grey;
}

.toc-level-1 {
    font-size: 1.25rem;
    margin-top: 0.5rem;
    font-weight: bold;
}

.toc-level-2 {
    font-size: 1.1rem;
    margin-top: 0.3rem;
    margin-left: 0.8rem;
}

.toc-level-3 {
    font-size: 1.0rem;
    margin-top: 0.3rem;
    margin-left: 1.5rem;
}

.toc-level-4 {
    font-size: 0.9rem;
    margin-top: 0.3rem;
    margin-left: 2.2rem;
}

.toc-active, .toc-common:hover {
    color: #000;
}

.toc-parent-active {
    color: #333;
}

@media (max-width: 90rem) {
    .toc-app-common {
        visibility: hidden;
    }
}

#wrap_toc{
    padding-bottom: 1.3rem;
}
</div>

210~220번째 줄에 있다

toc-elements-in-article 추가

4. script 추가

</body>위에 <script src="./images/toc.js"></script>를 추가한다.

맨 끝에 있다.

<script src="./images/toc.js"></script>
</body>

 

 

끝이다.

이제 각 블로그 글마다 차례가 나타날 것이다.

차례가 나타나지 않는 경우는 글 작성할 때 제목1, 제목2, 제목3을 사용하지 않은 경우다.

티스토리 제목 스타일

제목 스타일을 적용해야지만 차례가 나타난다.

 

코드

toc.js

 

추가한 부분

const TOC_CARD = (function () {
    const TocCardController = function () {
        const registerHTagsOnTocCard = function () {
            tocCardService.registerTagsOnTocInArticle(levelMap);
        }

        const onresize = function () {
            const width = document.body.clientWidth;
            if(width > 1420){
                tocCardService.removeTOCInArticle();
            }else{
                tocCardService.addTOCInArticle();
            }
        }

        return {
            onresize
        };
    };
    
    const TocCardService = function () {        
        const tocElementsCardInArticle = document.querySelector('#toc-elements-in-article');
        const wrapToc = document.querySelector('#wrap_toc');
        
        const removeTOCInArticle = function(){
            if(wrapToc.childNodes.length > 0){
                wrapToc.removeChild(tocElementsCardInArticle);
            }
        }
        const addTOCInArticle = function(){
            if(wrapToc.childNodes.length === 0){
                wrapToc.appendChild(tocElementsCardInArticle);
            }
        }              

        const registerTagsOnTocInArticle = function (levelMap) {
            if(hTags.length > 0){
                const tocH = document.createElement("h2");
                tocH.append("차례");
                tocElementsCardInArticle.appendChild(tocH);
            }
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCardInArticle.appendChild(hTagItem);
            });


            const width = document.body.clientWidth;
            if(width > 1420){
                removeTOCInArticle();
            }
        }

        return {
            registerTagsOnTocInArticle,
            removeTOCInArticle,
            addTOCInArticle
        }
    };
        

    const onresize = function () {
        tocCardController.onresize();
    }

    return {
        onresize
    }
})();
        

window.onresize = function(){
    TOC_CARD.onresize();
}

 

 

전체

/*
 * Tistory TOC (Table Of Contents)
 * dev by wbluke (wbluke.com)
 * last update 2020.05.06
 * version 0.1.5
 * https://github.com/wbluke/tistory-table-of-contents
 * https://www.wbluke.com/21
 */

const CLASS_OF_MAIN_CONTENTS = '.article-view';

const CONSTANTS = (function () {
    const KEY_OF_H1 = 1;
    const KEY_OF_H2 = 2;
    const KEY_OF_H3 = 3;
    const KEY_OF_H4 = 4;

    const LEVEL_1 = 1;
    const LEVEL_2 = 2;
    const LEVEL_3 = 3;
    const LEVEL_4 = 4;

    /* 최상위 태그에 따른 레벨 Map */
    const levelsByH1 = function () {
        return new Map([[KEY_OF_H1, LEVEL_1], [KEY_OF_H2, LEVEL_2], [KEY_OF_H3, LEVEL_3], [KEY_OF_H4, LEVEL_4]])
    }

    const levelsByH2 = function () {
        return new Map([[KEY_OF_H2, LEVEL_1], [KEY_OF_H3, LEVEL_2], [KEY_OF_H4, LEVEL_3]])
    }

    const levelsByH3 = function () {
        return new Map([[KEY_OF_H3, LEVEL_1], [KEY_OF_H4, LEVEL_2]])
    }

    const levelsByH4 = function () {
        return new Map([[KEY_OF_H4, LEVEL_1]])
    }

    return {
        indexOfH1: KEY_OF_H1,
        indexOfH2: KEY_OF_H2,
        indexOfH3: KEY_OF_H3,
        indexOfH4: KEY_OF_H4,
        levelsByH1: levelsByH1(),
        levelsByH2: levelsByH2(),
        levelsByH3: levelsByH3(),
        levelsByH4: levelsByH4(),
    }
})();

const TOC_CARD = (function () {
    const TocCardController = function () {

        const tocCardService = new TocCardService();

        const initTocElementsCard = function () {
            tocCardService.initTocElementsCard();
        }

        const giveIdToHTags = function () {
            tocCardService.giveIdToHTags();
        }

        const registerHTagsOnTocCard = function () {
            const levelMap = tocCardService.getLevelsByHighestTag();

            tocCardService.registerTagsOnToc(levelMap);
            tocCardService.registerTagsOnTocInArticle(levelMap);
        }

        const init = function () {
            const existsHTags = tocCardService.checkExistenceOfHTags();
            if (existsHTags) {
                initTocElementsCard();
                giveIdToHTags();
                registerHTagsOnTocCard();
            }
        };

        const onscroll = function () {
            const tocTag = tocCardService.findCurrentHTag();

            if (tocTag) {
                tocCardService.markCurrentHTag(tocTag);
                tocCardService.scrollToMainTocTag(tocTag);
                tocCardService.detectTocCardPosition();
            }
        }

        const onresize = function () {
            const width = document.body.clientWidth;
            if(width > 1420){
                tocCardService.removeTOCInArticle();
            }else{
                tocCardService.addTOCInArticle();
            }
        }

        return {
            init,
            onscroll,
            onresize
        };
    };

    const TocCardService = function () {
        const tocElementsCard = document.querySelector('#toc-elements');
        const tocElementsCardInArticle = document.querySelector('#toc-elements-in-article');
        const wrapToc = document.querySelector('#wrap_toc');

        const removeTOCInArticle = function(){
            if(wrapToc.childNodes.length > 0){
                wrapToc.removeChild(tocElementsCardInArticle);
            }
        }
        const addTOCInArticle = function(){
            if(wrapToc.childNodes.length === 0){
                wrapToc.appendChild(tocElementsCardInArticle);
            }
        }

        const mainContents = document.querySelector(CLASS_OF_MAIN_CONTENTS);

        const hTags = (function () {
            const foundHTags = mainContents.querySelectorAll('h1, h2, h3, h4');

            /* 글 내용 밑에 있는 [...카테고리의 다른 글] h4 제거 */
            return [...foundHTags].filter(hTag =>
                !hTag.parentElement.classList.contains('another_category') &&
                !hTag.parentElement.classList.contains('tags') &&
                !hTag.parentElement.classList.contains('related-articles'));
        })();

        /* h1, h2, h3, h4 태그가 있는지 확인한다 */
        const checkExistenceOfHTags = function () {
            if (mainContents === undefined) {
                return false;
            }

            return hTags.length !== 0;
        }

        const initTocElementsCard = function () {
            tocElementsCard.classList.add('toc-app-common', 'toc-app-items', 'toc-app-basic');
        }

        /** 최상위 태그에 따른 레벨 Map 받아오기
         *
         * h1 ~ h4 태그 중 가장 높은 태그를 찾아서 그에 맞게 Level을 설정한다.
         * 예를 들어, h1 태그가 없고 h2, h3 태그만 있는 경우
         * h2가 가장 높은 태그이며, 해당 태그 h2에 LEVEL_1을 부여하고 그 다음 태그인 h3에는 LEVEL_2를 부여한다.
         *
         * 부여된 Level에 따라 적용되는 CSS가 달라진다.
         * */
        const getLevelsByHighestTag = function () {
            const levelMapByHighestTag = {
                'H1': CONSTANTS.levelsByH1,
                'H2': CONSTANTS.levelsByH2,
                'H3': CONSTANTS.levelsByH3,
            };

            return levelMapByHighestTag[findHighestHTag().tagName] || CONSTANTS.levelsByH4;
        }

        /* 최상위 태그 판별 작업 */
        const findHighestHTag = function () {
            return [...hTags].reduce((pre, cur) => {
                const tagNumOfPre = parseInt(pre.tagName[1]);
                const tagNumOfCur = parseInt(cur.tagName[1]);

                return (tagNumOfPre < tagNumOfCur) ? pre : cur;
            });
        }

        /* TOC에 태그 삽입 */
        const registerTagsOnToc = function (levelMap) {
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCard.appendChild(hTagItem);
            });
        }

        const registerTagsOnTocInArticle = function (levelMap) {
            if(hTags.length > 0){
                const tocH = document.createElement("h2");
                tocH.append("차례");
                tocElementsCardInArticle.appendChild(tocH);
            }
            hTags.forEach((hTag, indexOfHTag) => {
                let hTagItem;
                levelMap.forEach((level, key) => {
                    if (hTag.matches(`h${key}`)) {
                        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
                    }
                })
                tocElementsCardInArticle.appendChild(hTagItem);
            });


            const width = document.body.clientWidth;
            if(width > 1420){
                removeTOCInArticle();
            }
        }

        const createTagItemByLevel = function (level = CONSTANTS.NUM_OF_H1, hTag, indexOfHTag) {
            const basicItem = createBasicItemBy(hTag, indexOfHTag);
            appendScrollEventsOn(basicItem, indexOfHTag);

            basicItem.classList.add(`toc-level-${level}`);

            return basicItem;
        }

        const createBasicItemBy = function (hTag, indexOfHTag) {
            const basicItem = document.createElement('a');

            basicItem.innerHTML += hTag.innerText;
            basicItem.id = `toc-${indexOfHTag}`;
            basicItem.classList = 'toc-common';

            return basicItem;
        }

        const generateIdOfHTag = function (indexOfHTag) {
            return 'h-tag-' + indexOfHTag;
        }

        const appendScrollEventsOn = function (basicItem, indexOfHTag) {
            const target = document.querySelector('#' + generateIdOfHTag(indexOfHTag));
            basicItem.addEventListener('click', () => window.scrollTo({
                top: target.offsetTop - 10,
                behavior: 'smooth'
            }));
        }

        const giveIdToHTags = function () {
            hTags.forEach((hTag, indexOfHTag) => {
                hTag.id = generateIdOfHTag(indexOfHTag);
            });
        }

        const findCurrentHTag = function () {
            if (hTags.length == 0) {
                return undefined;
            }

            const currentHTag = findCurrentMainHTag();
            return findTocTagCorrespondingToHTag(currentHTag);
        }

        const findCurrentMainHTag = function () {
            const headArea = document.querySelector('.area_head');

            let headAreaHeight = 0;
            if (headArea) {
                headAreaHeight = headArea.offsetHeight;
            }

            const middleHeight = window.scrollY + (window.innerHeight / 2) - headAreaHeight;

            return [...hTags].reduce((pre, cur) => {
                if (middleHeight < pre.offsetTop && middleHeight < cur.offsetTop) {
                    return pre;
                }

                if (pre.offsetTop < middleHeight && middleHeight <= cur.offsetTop) {
                    return pre;
                }

                return cur;
            });
        }

        const findTocTagCorrespondingToHTag = function (currentHTag) {
            const indexOfHTag = parseIndexOfTag(currentHTag);

            return document.querySelector(`#toc-${indexOfHTag}`);
        }

        const parseIndexOfTag = function (hTag) {
            const tokens = hTag.id.split('-');
            return parseInt(tokens[tokens.length - 1]);
        }

        const markCurrentHTag = function (tocTag) {
            removeAllClassOnTocTags('toc-active');
            tocTag.classList.add('toc-active');
            markParentHTagOf(tocTag);
        }

        const removeAllClassOnTocTags = function (className) {
            Array.prototype.slice.call(tocElementsCard.children).forEach(child => {
                child.classList.remove(className);
            });
        }

        const markParentHTagOf = function (tocTag) {
            const indexOfTocTag = parseIndexOfTag(tocTag);
            const levelOfBaseTocTag = findLevelOfTocTag(tocTag);

            removeAllClassOnTocTags('toc-parent-active');
            compareLevelAndMark(levelOfBaseTocTag, indexOfTocTag);
        }

        /**
         * 현재 active 태그의 부모 레벨 태그를 표시
         * 기준 태그(active 태그)애서 하나씩 위로 올라가면서 부모 태그를 탐색 (재귀)
         * */
        const compareLevelAndMark = function (levelOfBaseTocTag, indexOfCurrentTocTag) {
            if (levelOfBaseTocTag <= 1 || indexOfCurrentTocTag < 0) {
                return;
            }

            const currentTocTag = document.querySelector(`#toc-${indexOfCurrentTocTag}`);
            const levelOfCurrentTocTag = findLevelOfTocTag(currentTocTag);

            if (levelOfBaseTocTag <= levelOfCurrentTocTag) {
                return compareLevelAndMark(levelOfBaseTocTag, indexOfCurrentTocTag - 1);
            }

            currentTocTag.classList.add('toc-parent-active')
            compareLevelAndMark(levelOfBaseTocTag - 1, indexOfCurrentTocTag - 1);
        }

        const findLevelOfTocTag = function (tocTag) {
            const classes = tocTag.classList;
            if (classes.contains('toc-level-4')) {
                return 4;
            }

            if (classes.contains('toc-level-3')) {
                return 3;
            }

            if (classes.contains('toc-level-2')) {
                return 2;
            }

            return 1;
        }

        /**
         * TOC 항목이 너무 많아 TOC Card에 스크롤이 생길 경우,
         * 스크롤 이벤트에 따라 활성화된 TOC 태그가 보이도록 TOC Card의 스크롤도 함께 이동한다.
         */
        const scrollToMainTocTag = function (tocTag) {
            tocElementsCard.scroll({
                top: tocTag.offsetTop - (tocTag.offsetParent.offsetHeight * 0.3),
                behavior: 'smooth'
            });
        }

        const detectTocCardPosition = function () {
            const currentScrollTop = document.documentElement.scrollTop;

            const footer = document.querySelector('#mEtc');

            let footerTop = Number.MAX_SAFE_INTEGER;
            if (footer) {
                footerTop = footer.offsetTop;
            }

            const elementsCardBottom = currentScrollTop + tocElementsCard.offsetHeight;

            tocElementsCard.classList.remove('toc-app-basic', 'toc-app-bottom');

            if (elementsCardBottom >= footerTop) {
                tocElementsCard.classList.add('toc-app-bottom');
                return;
            }

            tocElementsCard.classList.add('toc-app-basic');
        }

        return {
            checkExistenceOfHTags,
            initTocElementsCard,
            getLevelsByHighestTag,
            registerTagsOnToc,
            giveIdToHTags,
            findCurrentHTag,
            markCurrentHTag,
            scrollToMainTocTag,
            detectTocCardPosition,
            registerTagsOnTocInArticle,
            removeTOCInArticle,
            addTOCInArticle
        }
    };

    const tocCardController = new TocCardController();

    const init = function () {
        tocCardController.init();
    };

    const onscroll = function () {
        tocCardController.onscroll();
    }

    const onresize = function () {
        tocCardController.onresize();
    }

    return {
        init,
        onscroll,
        onresize
    }
})();

TOC_CARD.init();

/**
 * scroll 시 현재 내용의 위치를 스크롤 이벤트를 통해 TOC에 표시해주기
 */
window.onscroll = function () {
    TOC_CARD.onscroll();
}

window.onresize = function(){
    TOC_CARD.onresize();
}

 

toc.css

추가한 부분

#wrap_toc{
    padding-bottom: 1.3rem;
}

 

 

전체

/* custom card style */

.toc-app-common {
    display: inline-block;
    padding: 1rem;
}

.toc-app-basic {
    position: fixed;
}

.toc-app-bottom {
    position: absolute;
}

/* custom item style */

.toc-app-items {
    height: 80vh;
    width: 15.3rem;
    overflow-y: scroll;
    -ms-overflow-style: none;
}

.toc-app-items::-webkit-scrollbar {
    display: none !important;
}

.toc-common {
    display: block;
    cursor: pointer;
    color: grey;
}

.toc-level-1 {
    font-size: 1.25rem;
    margin-top: 0.5rem;
    font-weight: bold;
}

.toc-level-2 {
    font-size: 1.1rem;
    margin-top: 0.3rem;
    margin-left: 0.8rem;
}

.toc-level-3 {
    font-size: 1.0rem;
    margin-top: 0.3rem;
    margin-left: 1.5rem;
}

.toc-level-4 {
    font-size: 0.9rem;
    margin-top: 0.3rem;
    margin-left: 2.2rem;
}

.toc-active, .toc-common:hover {
    color: #000;
}

.toc-parent-active {
    color: #333;
}

@media (max-width: 90rem) {
    .toc-app-common {
        visibility: hidden;
    }
}

#wrap_toc{
    padding-bottom: 1.3rem;
}