GeehDev

[티스토리] 게시글 내 목차 만들기 본문

Notes/DayLog

[티스토리] 게시글 내 목차 만들기

geehyun 2025. 2. 21. 14:57

Index

    반응형

    [티스토리] 게시글 내 목차 만들기


    개요

    ✅기능

    • 게시글 내 h태그를 이용하여 목차를 자동 생성할 수 있고
    • 각 각 목차를 클릭 시 해당 내용으로 이동하도록 (id와 a태그의 href="#아이디" 방식 사용)
    • 현재 스킨이 반응형이기 때문에 그에 따라 목차도 반응형으로 구성 (접이식 목차 / 기본목차)

    방식

    • 게시글 작성 시 제목1, 제목2, 제목3 (h태그)를 사용하여 구현 예정
    • 티스토리 내 스킨편집과 서식 기능을 이용해서 구현 예정 - html / css / js 작성

    목차를 만드려는 이유

    기존 velog에서 티스토리로 이사 오면서 여러 가지 차이점과, 각 각의 장단점을 느꼈었습니다.

    그중 velog에서는 마크다운 문법으로 글을 작성하면 알아서 목차가 구성되는데, 티스토리의 경우 스킨에 따라 목차가 있을 수 있지만, 제가 택한 스킨에는 없어서 이번 기회에 추가해 보기로 했습니다.

    Velog 에서 자동 생성되는 목차 : 클릭 시 해당 내용으로 이동 됨.

     

    여러 가지 글들을 찾아봤지만, 그냥 티스토리 내 스킨편집 기능을 이용해 스스로 작업해보려고 합니다.(스스로 불러온 재앙...)


    작업 진행

    1. HTML작성

    목차 HTML 을 작성합니다.
    해당 HTML 목차는 티스토리 스킨편집에서 게시글 본문에 해당하는 위치를 확인하고 해당 위치에 넣어줄 예정

    <!-- 목차 영역 made by GeehDev -->
    <div id="indexArea" class="index-area">
        <!-- 목차 제목  made by GeehDev -->
        <h3 class="index-area-title">목차</h2>
        <!-- // 목차 제목 made by GeehDev -->
    
        <!-- 목차 내용 made by GeehDev -->
        <ul class="index-area-contents"></ul>
        <!-- // 목차 내용 made by GeehDev -->
    </div>
    <!-- // 목차 영역 made by GeehDev -->

    2. CSS 작성

    CSS는 저는 우선 간단하게 구성했습니다.

    • 접이식 목차 (우측에 고정되어 있으면서 펼쳐서 보거나 접어놓거나 할 수 있도록)
    • 기본 목차 (게시글 상단에 위치)
    /* 목차 컨테이너 스타일 */
    #indexArea {
      border: 1px solid #efefef;
      border-radius: 10px;
      padding: 10px 25px;
      box-sizing: border-box;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      background-color: #fff;
      width: 400px;
      z-index: 100;
      font-size: 15px;
      position: fixed;
      top: 100px;
      right: -400px; /* 기본적으로 숨김 */
      transition: all 0.3s ease-in-out;
    }
    
    /* 열렸을 때의 상태 */
    #indexArea.open {
      right: 10px; /* 나타나게 함 */
    }
    
    /* 목차 제목 */
    #indexArea > .index-area-title {
      font-weight: bold;
    }
    
    /* 목차 구분선 */
    #indexArea h4 {
      margin: 5px 0;
      padding: 0 5px;
      color: #b3b3b3;
    }
    #indexArea h4::after {
      content: "";
      display: block;
      height: 1px;
      background-color: #ccc;
      margin-top: 10px;
    }
    
    /* 목차 리스트 스타일 */
    #indexArea ul {
      padding: 0;
    }
    #indexArea ul li {
      list-style-type: none;
      margin: 0;
      line-height: 1.8;
    }
    
    /* 목차 링크 스타일 */
    #indexArea ul li a {
      color: #555;
      text-decoration: none;
      transition: all 0.2s ease;
    }
    #indexArea ul li a:hover {
      color: #6bacce;
    }
    #indexArea ul li a.active {
      text-decoration: underline;
      color: #6bacce;
      font-weight: bold;
    }
    
    /* 활성화된 목차 항목 앞에 강조 바 */
    #indexArea ul li a.active::before {
      content: "";
      display: inline-block;
      height: 10px;
      width: 5px;
      background-color: #6bacce;
      margin-right: 5px;
    }
    
    /* 깊이에 따른 들여쓰기 적용 */
    #indexArea ul li.depth1 {
      font-weight: bold;
    }
    #indexArea ul li.depth2 {
      margin-left: 1.5em;
    }
    #indexArea ul li.depth3 {
      margin-left: 3em;
    }
    
    /* 목차 토글 버튼 */
    .index-toggle-btn {
      position: fixed;
      top: 100px;
      right: 10px;
      background-color: #6bacce;
      color: #fff;
      padding: 5px 10px;
      border-radius: 5px;
      cursor: pointer;
      font-size: 12px;
      z-index: 101;
      transition: all 0.3s ease-in-out;
    }
    
    .index-toggle-btn.hidden {
      right: -50px; /* 숨김 */
    }
    
    /* 반응형 설정 */
    @media (max-width: 1500px) {
      #indexArea {
          position: relative;
          right: 0;
          top: 0;
          width: 100%;
          margin: 0 auto;
      }
      .index-toggle-btn {
          display: none;
      }
    }

    3. JS작성

    페이지를 로딩하면서 작성한 게시글의 html을 읽어와서 h태그들을 목차로 생성해 줄 js를 작성해 줌
    각 함수와 구조는 주석으로 작성해 놨습니다.

    /**
     * IndexGenerator 모듈
     * 블로그 게시글에서 h1, h2, h3 태그를 찾아 목차를 자동으로 생성
     */
    const IndexGenerator = (function() {
      const targetSelector = '#article-view > .contents_style';
      const targetHeading = ['h1', 'h2', 'h3'];
      let headings;
      
      /****************************************
       * 초기화 관련
       * - init                   : 모듈 초기화
       * - initField              : 필드 초기화
       * - gernerateComponets     : 컴포넌트 생성
       * - bindEvents             : 이벤트 바인딩
       ****************************************/
      const init = function() {
          initField();
          if(headings && headings.length > 0) {
              generateComponents();
              bindEvents();
          }
      };
    
      const initField = function() {
          headings = UTILS.getHeadings(targetSelector, targetHeading);
      };
    
      const generateComponents = function() {
          INDEX.gernerateIndex();
      };
    
      const bindEvents = function() {
          EVENTS.bindEvents();
      };
    
      /****************************************
       * UTIL 관련 메서드
       ****************************************/
      const UTILS = {
          /**
           * Heading 태그 가져오는 함수
           * @param {string} targetSelector - 헤딩을 검색할 대상 요소의 선택자 : 게시글 본문 선택자자
           * @param {Array<string>} targetHeading - 검색할 헤딩 태그 리스트 : h1, h2, h3
           * @returns {Array<Element>} - 찾은 헤딩 요소들의 배열
           */
          getHeadings: function(targetSelector, targetHeading) {
              let result = [];
              const contents = document.querySelector(targetSelector);
      
              if(contents) {
                  const headingTags = contents.querySelectorAll(targetHeading.join(','));
                  headingTags.forEach(el => result.push(el));
              }
              return result;
          },
          /**
           * 문자열 공란 체크 함수
          * @param {string} str - 문자열
          * @returns {boolean} - 공란일 경우 true, 공란이 아닐경우 false
           */
          isEmpty: function(str) {
              return !str || str.replace(/&nbsp;/g, '').trim().length === 0;
          },
          /**
           * heading 태그에 따라 depth 클래스 반환하는 함수
           * @param {string} tagName 
           * @returns {string} - 해당하는 depth 클래스명
           */
          getDepthClass: function(tagName) {
              switch(tagName) {
                  case 'H1': return 'depth1';
                  case 'H2': return 'depth2';
                  case 'H3': return 'depth3';
                  default: return 'depth1';
              }
          },
      };
    
      /****************************************
       * INDEX 관련 메서드
       ****************************************/
      const INDEX = {
          /**
           * 목차 생성 함수
           */
          gernerateIndex: function() {
              INDEX.makeIndexCustom();
              INDEX.makeIndexToggleBtn();
              INDEX.labelHeadings();
          },
          /**
           * 커스텀 목차 element 만들어서 indexArea에에 넣는 함수
           */
          makeIndexCustom: function() {
              const indexCustomEl = document.querySelector('#indexArea .index-area-contents');
              headings.forEach(el => {
                  if(!UTILS.isEmpty(el.textContent)) {
                      let liEl = document.createElement('li');
                      let aEl = document.createElement('a');
                      
                      aEl.innerText = el.textContent.trim();
                      aEl.href = '#' + el.textContent.trim();
                      
                      liEl.classList.add(UTILS.getDepthClass(el.tagName));
                      liEl.appendChild(aEl);
                      indexCustomEl.appendChild(liEl);
                  }
              });
          },
          /**
           * 목차 열기/닫기 토글 버튼 만드는 함수
           */
          makeIndexToggleBtn: function() {
              const toggleBtn = document.createElement('div');
              toggleBtn.classList.add('index-toggle-btn');
              toggleBtn.innerText = '< 목차 열기';
              document.body.appendChild(toggleBtn);
          },
          /** 
           * heading tag에 ID를 라벨링 함수 
           * */
          labelHeadings: function() {
              headings.forEach(el => {
                  el.setAttribute('id', el.textContent.trim());
              });
          },
      };
    
      /****************************************
       * EVENT 관련 메서드
       ****************************************/
      const EVENTS = {
          /**
           * 이벤트 바인드
           */
          bindEvents: function() {
              const toggleBtn = document.querySelector('.index-toggle-btn');
              const indexArea = document.querySelector('#indexArea');
              const indexEl = document.querySelectorAll('#indexArea .index-area-contents a');
              toggleBtn.addEventListener('click', ()=>{EVENTS.toggleIndexOnOff(toggleBtn, indexArea);});
              window.addEventListener('resize', ()=>{EVENTS.checkScreenSize(toggleBtn, indexArea);});
              EVENTS.checkScreenSize(toggleBtn, indexArea);
              indexEl.forEach(el => {
                  el.addEventListener('click', ()=>{EVENTS.highlightIndex(indexEl, el);});
              });
          },
          /**
           * 화면 사이즈 체크 함수
           * 1501 기준으로 접이식 목차, 그 보다 적을 경우는 기본 목차
           */
          checkScreenSize: function(toggleBtn, indexArea) {            
              if (window.innerWidth >= 1501) {
                  indexArea.classList.remove('open');
                  toggleBtn.classList.remove('hidden');
              } else {
                  indexArea.classList.add('open');
                  toggleBtn.classList.add('hidden');
              }
          },
          /**
           * 인덱스 목차 열고 닫는 함수
           */
          toggleIndexOnOff: function(toggleBtn, indexArea) {
              indexArea.classList.toggle('open');
              toggleBtn.innerText = indexArea.classList.contains('open') ? '> 목차 닫기' : '< 목차 열기';
          },
          /**
           * 선택된 요소 하이라이트 표시하는 함수수
           * @param {Array<Element>} indexEl - 인덱스 각 a 태그 element 리스트트
           * @param {element} el - 클릭된 element
           */
          highlightIndex: function(indexEl, el) {
              indexEl.forEach(other => {
                  other.classList.remove('active');
              });
              el.classList.add('active');
          }
      };
    
      
    
      return {
          init: () => {init();}
      };
    })();
    
    // 페이지가 로드될 때 자동으로 IndexGenerator 초기화 실행
    /**
    * 문서가 완전히 로드된 후 IndexGenerator를 실행한다.
    */
    document.addEventListener('DOMContentLoaded', IndexGenerator.init);

    4. 블로그 관리에서 서식 입히기

     

    블로그 관리 - 스킨편집 - html 편집 클릭하여 스킨을 편집할 수 있습니다.

    1) html 추가하기

    html 탭을 클릭하고 본인의 스킨의 html 구조에서 게시글 본문에 해당하는 곳을 찾습니다.
    저의 경우는

    <s_article_rep>

          <s_permalink_article_rep>

                <div class="area_view" id="article-view"> 내 였습니다.
    해당 위치를 찾아서 해당 태그 내 치환자 위쪽으로 만들어둔 html을 넣습니다.

    2) css, js 파일 넣기

    파일업로드 탭을 클릭하여 [추가]버튼을 클릭해서 위에 만들어둔 css, js 파일을 업로드합니다.

     

    2) css, js 포함시키기

    다시 html 탭을 클릭해서 <head> 태그를 찾습니다.
    <head>태그 가장 하위에 방금 업로드한 css, js 경로를 작성합니다.

    완성된 모습

    • 접이식 목차 - 펼친 상태

    접이식 목차 - 펼친 상태

    • 접이식 목차 - 접힌 상태

    접이식 목차 - 접힌 상태

    • 기복 목차

    기본목차

     

    마치며

    velog에서 티스토리로 넘어오면서 스킨에 대해 거의 처음 건드려보는 건데 생각보다 직접 html, css, js를 건들 수 있어서 꽤나 재밌는 시간이었던 것 같습니다.

     

    다만, 앞으로는 여러 금손님들이 만들어두신 여러 가지 좋은 스킨들을 활용하는 게 좋겠다고 생각합니다.🤣

    혹시라도....혹시라도 제 글을 보고 직접 작업하시는 분이 계신다면.... 저는 제 스킨에 맞게 css 설정등을 하였으니 이대로 쓰시기에는 아마 안 맞는 부분이 꽤나 있을 것 같습니다.

     

    방식만 참고하시고.... 본인 스킨에 맞춰서 더 멋진 목차를 만드시고.... 저도 알려주셨으면 좋겠습니다.😭😭😭

    728x90