본문 바로가기
카테고리 없음

[GAS] 필터기능을 이용한 검색기능 구현

by 일등미노왕국 2024. 6. 30.

 

승마를 배우고 있는 소율이에게 선물을 주고자 만들어본 녀석이다.

 

2023년 제주도 승마대회의 자료부터 현재까지를 스프레드시트에 업로드한후 그 데이터를 기준으로 반응형 웹을 만들어 보았다.

 

최초에는 리스트가 10개만 보이고 검색어를 입력하면 그에 맞는 검색 내용이 필터링 되어서 나오게 된다. 

마명앞의 말 이모티콘을 클릭을 하게 되면 해당말을 타고 입상한 데이터들이 나오게 된다.

 

나중에 홈페이지를 제작해서 해당 페이지를 삽입할 예정이다. ...언제일지는 모르겠지만..ㅋㅋㅋ

 

혹시라도 이것을 이용해보는 사람들중 오타나 잘못된 정보가 있으면 댓글로 이야기 해주면 내용을 수정해서 한국 승마사업에 이바지(?)하도록 하겠다..

 

GS코드

더보기
// 웹 앱의 진입점 함수
function doGet() {
  // 'search'라는 이름의 HTML 파일을 로드하여 웹 페이지 생성
  return HtmlService.createHtmlOutputFromFile('search')
      // IFRAME 샌드박스 모드 설정 (보안을 위해)
      .setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

// 스프레드시트에서 데이터를 가져오는 함수
function getDataFromSpreadsheet(spreadsheetId, sheetName, range) {
  // 주어진 ID로 스프레드시트 열기
  var ss = SpreadsheetApp.openById(spreadsheetId);
  
  // 지정된 이름의 시트 가져오기
  var sheet = ss.getSheetByName(sheetName);
  
  // 지정된 범위의 데이터 가져오기
  var data = sheet.getRange(range).getValues();
  
  // 가져온 데이터 반환
  return data;
}

// 스프레드시트 편집 시 자동으로 실행되는 함수
function onEdit(e) {
  // 현재 활성화된 시트 가져오기
  var sheet = SpreadsheetApp.getActiveSheet();
  
  // 현재 선택된 셀의 범위 가져오기
  var range = sheet.getActiveCell();
  
  // 선택된 셀의 행과 열 번호 가져오기
  var row = range.getRow();
  var col = range.getColumn();
  
  // B3 셀(3행 2열)에서 편집이 일어났다면
  if (row == 3 && col == 2) {
    // B3 셀을 다시 활성화 (포커스 유지)
    sheet.getRange(row, col).activate();
  }
}

HTML코드

더보기
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <!-- 반응형 웹 디자인을 위한 뷰포트 설정 -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1">
  <title>HorseTop in Jeju</title>
  <!-- Bootstrap CSS 링크 -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
  <!-- SweetAlert2 CSS 링크 -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.7.3/dist/sweetalert2.min.css">
  <style>
    /* 테이블 스타일 설정 */
    .table.table-hover {
      font-size: 1rem;
      font-weight: bold;
      white-space: nowrap; /* 줄바꿈 방지 */
    }
    /* 네비게이션 링크 커서 스타일 */
    .nav-link {
      cursor: pointer;
    }
    /* 테이블 셀 스타일 */
    .table td {
      font-size: 0.9rem;
      white-space: nowrap; /* 줄바꿈 방지 */
    }
    /* 하이라이트 스타일 (순위가 있는 행) */
    .highlight {
      background-color: #FFA500; /* 주황색 배경 */
    }
    /* 모바일 버전에서 숨길 열 */
    .mobile-hide {
      display: none;
    }
    /* 열 크기 고정 */
    .table th, .table td {
      width: 100px;
      max-width: 100px;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  </style>
</head>
<body>
  <!-- SweetAlert2 스크립트 -->
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.7.3/dist/sweetalert2.all.min.js"></script>
  
  <div class="container">
    <!-- 네비게이션 탭 -->
    <ul class="nav nav-tabs">
      <li class="nav-item">
        <div class="nav-link" id="search-link">HorseTop in Jeju</div>
      </li>
    </ul>

    <!-- 메인 앱 컨테이너 -->
    <div id="app">
      <!-- 검색 입력 필드 -->
      <div class="mt-3">
        <input type="text" class="form-control" id="searchInput" placeholder="검색할 내용을 입력하세요">
      </div>
      <!-- 페이지 크기 설정 버튼 -->
      <div class="mt-3">
        <button class="btn btn-primary" onclick="setPageSize(5)">5</button>
        <button class="btn btn-primary" onclick="setPageSize(10)">10</button>
        <button class="btn btn-primary" onclick="setPageSize(20)">20</button>
        <button class="btn btn-primary" onclick="setPageSize(50)">50</button>
        <button class="btn btn-primary" onclick="setPageSize(Infinity)">All</button>
      </div>
      <!-- 결과 테이블 -->
      <div class="mt-3 table-wrapper">
        <table class="table table-hover">
          <thead>
            <tr>
              <!-- 테이블 헤더 -->
              <th scope="col" class="pc-show">대회명</th>
              <th scope="col" class="pc-show">대회일시</th>
              <th scope="col" class="pc-show">종목</th>
              <th scope="col" class="pc-show">부명</th>
              <th scope="col" class="pc-show">선수명</th>
              <th scope="col" class="pc-show">마명</th>
              <th scope="col" class="pc-show">소속</th>
              <th scope="col" class="pc-show">비고</th>
              <th scope="col" class="pc-show">순위</th>
              <th scope="col" class="pc-show">기록</th>
            </tr>
          </thead>
          <tbody class="mt-3" id="searchResults">
            <!-- 검색 결과가 여기에 동적으로 추가됨 -->
          </tbody>
        </table>
      </div>
    </div>
  </div>

  <script>
    var data; // 전체 데이터를 저장할 변수
    var pageSize = 10; // 기본 페이지 크기는 10

    // Google Apps Script에서 데이터를 가져오는 함수
    function setDataForSearch() {
      google.script.run.withSuccessHandler(function(dataReturned) {
        console.log("Data loaded:", dataReturned); // 디버그 로그
        data = dataReturned;
        displayResults(sortByDate(data), pageSize); // 데이터를 대회일시로 정렬하여 결과를 표시
      }).getDataFromSpreadsheet("1RF73BtYoku01crf4iwamz4cE8-iJQMlnImAOHBRqMJU", "LIST", "A2:J");
    }

    // 데이터를 대회일시 기준으로 정렬하는 함수
    function sortByDate(data) {
      return data.sort(function(a, b) {
        let dateA = parseStartDate(a[1]);
        let dateB = parseStartDate(b[1]);
        return dateB - dateA; // 내림차순 정렬
      });
    }

    // 대회일시에서 시작일을 추출하여 Date 객체로 변환하는 함수
    function parseStartDate(dateRangeStr) {
      let startDateStr = dateRangeStr.split('-')[0].trim();
      return parseDate(startDateStr);
    }

    // 년월일 형식의 문자열을 Date 객체로 변환하는 함수
    function parseDate(dateStr) {
      let parts = dateStr.split('.');
      let year = parseInt(parts[0], 10);
      let month = parseInt(parts[1], 10) - 1; // JavaScript의 month는 0부터 시작하므로 -1 처리
      let day = parseInt(parts[2], 10);
      return new Date(year, month, day);
    }

    // 검색 결과를 화면에 표시하는 함수
    function displayResults(results, limit) {
      let searchResultsBox = document.getElementById("searchResults");
      searchResultsBox.innerHTML = "";

      // 페이지 크기에 맞게 결과 개수 제한
      results = results.slice(0, limit);

      results.forEach(function (r) {
        let tr = document.createElement("tr");

        for (let i = 0; i < r.length; i++) {
          let td = document.createElement("td");

          // 기록 열(인덱스 9)은 문자열로 처리
          if (i === 9) {
            td.textContent = r[i].toString();
          } else {
            if (i === 5) {
              // 마명 열에 search 아이콘 추가
              td.innerHTML = `<img src="https://cdn-icons-png.flaticon.com/512/4081/4081978.png" alt="search icon" width="20" height="20" onclick="openPopup('${r[5]}')"> ${r[i]}`;
            } else {
              td.textContent = r[i];
            }
          }

          // 모바일 버전에서 1, 3, 7, 8 번째 열에 mobile-hide 클래스 추가
          if (window.innerWidth <= 1000 && (i === 1 || i === 3 || i === 7)) {
            td.classList.add('mobile-hide');
          }

          tr.appendChild(td);
        }

        // 순위 열(인덱스 8)이 null이 아닌 경우에만 highlight 클래스 추가
        if (r[8] !== "") {
          tr.classList.add('highlight');
        }

        searchResultsBox.appendChild(tr);
      });

      toggleColumns(); // 결과 표시 후 열 숨김 처리
    }

    // 검색 함수
    function search() {
      let searchInput = document.getElementById("searchInput").value.toString().toLowerCase().trim();
      let searchWords = searchInput.split(/\s+/);
      let searchColumns = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; // 검색할 컬럼 인덱스

      let resultsArray = searchInput === "" ? data : data.filter(function(r){
        return searchWords.every(function(word){
          return searchColumns.some(function(colIndex){
            return r[colIndex].toString().toLowerCase().indexOf(word) !== -1;
          });
        });
      });

      displayResults(sortByDate(resultsArray), searchInput === "" ? pageSize : Infinity); // 검색된 결과를 대회일시로 정렬하여 표시
    }

    // 페이지 크기를 설정하는 함수
    function setPageSize(size) {
      pageSize = size;
      search(); // 페이지 크기 변경 후 검색 결과 갱신
    }

    // 검색 링크 클릭 이벤트 리스너
    document.getElementById("search-link").addEventListener("click", function(){
      console.log("search-link clicked"); // 디버그 로그
      setDataForSearch();
    });

    // 검색 입력 이벤트 리스너
    document.getElementById("app").addEventListener("input", function(e){
      if(e.target.matches("#searchInput")){
        console.log("searchInput input event detected"); // 디버그 로그
        search();
      }
    });

    // 화면 크기에 따라 열 숨김 처리하는 함수
    function toggleColumns() {
      const isMobile = window.innerWidth <= 1000;
      const thElements = document.querySelectorAll('.table th');
      const tdElements = document.querySelectorAll('.table td');

      thElements.forEach((th, index) => {
        if (isMobile && (index === 1 || index === 3 || index === 7)) {
          th.classList.add('mobile-hide');
        } else {
          th.classList.remove('mobile-hide');
        }
      });

      tdElements.forEach((td, index) => {
        const columnIndex = index % 10; // td의 전체 인덱스에서 열 인덱스 계산
        if (isMobile && (columnIndex === 1 || columnIndex === 3 || columnIndex === 7)) {
          td.classList.add('mobile-hide');
        } else {
          td.classList.remove('mobile-hide');
        }
      });
    }

    // 팝업을 열어 말의 상세 정보를 표시하는 함수
    function openPopup(horseName) {
      const rankedData = data.filter(row => row[5] === horseName && row[8] !== "");

      // HTML 생성
      let htmlContent = `<h2>${horseName}</h2><table class="table">`;
      htmlContent += "<thead><tr><th>대회명</th><th>종목</th><th>선수명</th><th>소속</th><th>순위</th><th>기록</th></tr></thead><tbody>";
      rankedData.forEach(row => {
        htmlContent += "<tr>";
        [0, 2, 4, 6, 8, 9].forEach(i => {
          htmlContent += `<td>${row[i]}</td>`;
        });
        htmlContent += "</tr>";
      });
      htmlContent += "</tbody></table>";

      // SweetAlert2를 사용하여 모달 표시
      Swal.fire({
        html: htmlContent,
        width: 800,
        showCloseButton: true,
        showConfirmButton: false,
        didOpen: () => {
          window.addEventListener('resize', onResize, { once: true }); // 모달이 열릴 때 resize 이벤트 추가
        }
      });
    }

    // 화면 크기 변경 시 팝업을 다시 여는 함수
    function onResize() {
      const horseName = document.querySelector('.swal2-html-container h2').textContent;
      openPopup(horseName);
    }

    // 화면 크기 변경 이벤트 리스너
    window.addEventListener('resize', toggleColumns);

    // 초기 데이터 로드
    setDataForSearch();
  </script>
</body>
</html>

 

해당 웹앱을 이용할 수 있는 링크를 첨부한다.

https://script.google.com/macros/s/AKfycbxF9WxaRXo6oEBuVmGarMwHs93kwcLdc09sutkkT8VdU5-cX9TnTSTzaR7QuVb7Vqbgeg/exec

 

댓글