직무스킬_엑셀/스프레드시트 직장인라이프

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기

0
Please log in or register to do it.

구글 스프레드시트를 활용하다 보면 드라이브에 폴더링을 깔끔하게 하더라도 부족한 부분이 생기기 마련입니다.

또한 여러 데이터를 편리하게 관리하고 싶을 때, 링크 모음집을 만들어 사용해보신 분들이 있을 겁니다.

하지만 월 매출현황이라던가 매주 혹은 매월 작성되는 문서들이 있거나, 사용자들이 꾸준히 문서를 만들어나가는 경우에는 매번 링크를 달기가 쉽지 않은 일이겠죠.

이런 경우에 매우 요긴하게 쓰일 수 있는 자동화 Appscript를 소개합니다.

앱스 크립트란?

앱 스크립트(Apps Script)는 구글 워크스페이스 플랫폼에서 가벼운 애플리케이션 개발을 위해 
구글이 개발한 스크립팅 플랫폼이다. 구글 앱 스크립트는 처음에 마이크 함(Mike Harm)이 구글 시트의 개발자로서 일할 당시 부차적인 프로젝트로서 개발하였다. – 출처 위키백과

말 그대로 구글 스프레드시트를 극한까지 쓸 수 있는 환경을 제공해주는 사용자 커스터마이징 툴의 느낌이 강합니다.
매우 무섭게도 앱스 크립트 하나만으로도 거의 회사 운영 업무에 필요한 전반 (회계, 인사, 노무 등) 업무들을 대부분 자동화할 수 있습니다.

아쉽게도 오늘은 그러한 자동화를 다루진 않고, 자동화의 든든한 초석이 되어줄 문서 인덱스에 대해 소개합니다.

문서 인덱스?

목차를 만든다고 생각하시면 쉽습니다.
예를 들어, 6월 매출 현황표를 정리해놓은 폴더에 20개 지점의 매출 전표가 있다고 가정해봅시다. 이거 엑셀로 하나씩 다 보려면……
오늘은 치킨 각 이 아니라 야근각…!

거창하게 만든다면 도서관 같은 구조도 불가능하지 않습니다. 특히 당장 필요한 게 아니면 먼저 구글 드라이브의 폴더링에 대한 것부터
차근차근 이해하고 나시면 꽤 큰 데이터도 쉽게 정리할 수 있으니 너무 먼저 지레 겁먹는 일은 삼가도록 하겠습니다

구글 드라이브부터 알아가야 한다.

구글 드라이브를 써본 사람들은 어느 정도 이해가 빠르지만, 처음 접하는 이들을 위해서 간단히 소개를 하고 넘어가겠습니다
우선 구글 드라이브는 파일을 저장할 수 있는 온라인 스토리지 공간입니다. PDF부터 XLSX CSV 등 파일도 저장할 수 있으며,
구글 스프레드시트, 구글 폼, 구글 독스, 구글프레제넽이션 등 온라인 상에서 문서작성이 가능한 엄청난 장점을 지니고 있습니다.

이러한 특성 덕분에 구글 드라이브에 대해 잘 모르셨던 분들도 대략이나마 아 네이버 클라우드 같은 서비스구나 하면서 알정도의 인지도를 갖추고 있습니다.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 구글 드라이브부터 알아가야 한다.

먼저 우리가 이해해야 하는 것은 ‘폴더’라는 개념입니다.

드라이브의 가장 처음 화면에서 폴더를 만들어보면 위와 같이 /drive/folders/folder_ID 값이 만들어지게 됩니다.
앞으로 만들 문서 인덱스 자동화에 가장 큰 역할을 할 녀석이 바로 저 폴더에 존재하는 고유 ID값이 되게 됩니다.

왜 중요할까요?

1. 구글 드라이브는 문서 ID 기반으로 문서를 찾는다.

2. 문서 내의 콘텐츠는 언제든 바뀔 수 있기 때문. (제목 등)

3. 하위 문서들을 불러오기 위해 폴더의 단계가 많아질수록 스크립트의 난이도가 높아지게 된다.

아마 3번이 정확히 어떤 이야긴지 감이 안 오실 텐데요. 보통은 하나의 폴더에 모든 문서를 넣어놓는 게 아닌, 분류를 통해 해당 폴더의 하위 폴더 또 그 폴더들의 하위폴더 이런 식으로 계속 무한 증식하게 됩니다. (정리를 잘하시는 분들은 특히나…)

이렇게 되면 특정 문서를 찾기 위해 몇 개씩 폴더를 열어봐야 하는 일이 생기기도 하죠?
이럴 땐 폴더들끼리 구분만 잘해줘도 중간 이상은 가게 됩니다.

적용 예제

먼저 스프레드시트가 필요합니다. 없으신 분들은 새로 만들어 주세요. 이후 메뉴 탭에서 확장 프로그램 – Apps Script로 들어와 주세요.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 적용 예제
구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 적용 예제

충분히 혼란스러울 수 있지만 우리는 침착하게 

function myFunction() {

}

요 부분을 지워주고 아래 스크립트를 복사 붙여 넣기 해주세요.

var folderId = SpreadsheetApp.getActiveSpreadsheet().getRange('A1').getValues(); // The folder ID (everything after the 'folders/' portion of the URL).
var searchDepthMax = 100; // Max depth for recursive search of files and folders
var listFiles = true; // flag for listing files
var cacheTimeout = 24 * 60 * 60 * 1000; // set cache time-out
var lockWaitTime = 1 * 60 * 1000; // set maximium watiting time for the cache lock
var appendToSheet = false; // flag for appending to selected spreadsheet
var writeBatchSize = 100; // the write batch size


// ===========================================================================================================
// Global variables
var cacheOutputs = 'InventoryScript_outputs';
var cacheKillFlag = 'InventoryScript_killFlag';


// ===========================================================================================================
// Reset the script cache if it is required to run from the beginning
function reset() {
  SpreadsheetApp.getActiveSpreadsheet().toast('Reseting script...', 'Status', -1);
  
  // reset triggers and delete cache variables
  setKillFlag_(true, this.cacheTimeout);
  deleteTriggers_(this.loopResetGapTime);
  deleteCache_();
  
  SpreadsheetApp.getActiveSpreadsheet().toast('Reset is complete!', 'Status', -1);
}


// ===========================================================================================================
// List all folders and files, then write into the current spreadsheet.
function run() {
  SpreadsheetApp.getActiveSpreadsheet().toast('Executing script...', 'Status', -1);
  
  // load cache
  setKillFlag_(false, this.cacheTimeout);
  var outputRows = getCache_(this.lockWaitTime);
  
  // get list
  if (outputRows === undefined || outputRows === null ||
      outputRows[0] === undefined || outputRows[0] === null) {
    outputRows = [];
    
    outputRows = getChildFiles_(null, DriveApp.getFolderById(this.folderId), 
                                listFiles, cacheTimeout, outputRows);
    
    outputRows = getFolderTree_(outputRows, this.folderId, this.listFiles, this.cacheTimeout, 
                                this.lockWaitTime, this.searchDepthMax);
  }
  
  
  // write list
  writeFolderTree_(outputRows, this.appendToSheet);
  
  SpreadsheetApp.getActiveSpreadsheet().toast('Execution is complete!', 'Status', -1);
}


// ===========================================================================================================
// Get the list of folders and files
function getFolderTree_(outputRows, folderId, listFiles, cacheTimeout, lockWaitTime, searchDepthMax) {
  var parentFolder, sheet = null;
  var searchDepth = -1;
  
  try {
    // Get folder by id
    parentFolder = DriveApp.getFolderById(folderId);
    
    // Initialise the spreadsheet
    sheet = SpreadsheetApp.getActiveSheet();
    
    // Get files and/or folders
    outputRows = getChildFolders_(searchDepth, parentFolder.getName(), parentFolder, sheet,
                                  listFiles, cacheTimeout, lockWaitTime, outputRows, searchDepthMax);
  } catch (e) {
    SpreadsheetApp.getActiveSpreadsheet().toast('Timed out!', 'Status', -1);
  }
  
  return outputRows;
}


// ===========================================================================================================
// Write the list of folders and files into the spreadsheet
function writeFolderTree_(outputRows, appendToSheet) {
  var sheet = null;
  
  try {
    if (getKillFlag_() === false) {
      // Initialise the spreadsheet
      sheet = SpreadsheetApp.getActiveSheet();
      
      // Write to the selected spreadsheet
      writeOutputs_(sheet, outputRows, appendToSheet);
      
      // reset cache
      reset();
    }
  } catch (e) {
    SpreadsheetApp.getActiveSpreadsheet().toast('Timed out!', 'Status', -1);
  }
}


// ===========================================================================================================
// Get the list of folders and files and their metadata using a recursive loop
function getChildFolders_(searchDepth, parentFolderName, parentFolder, sheet, listFiles, cacheTimeout,
                          lockWaitTime, outputRows, searchDepthMax) {
  var childFolders = parentFolder.getFolders();
  var childFolder = null;
  searchDepth += 1;
  
  try{
    // List sub-folders inside the folder
    while (childFolders.hasNext() && searchDepth < searchDepthMax && getKillFlag_() === false) {
      childFolder = childFolders.next();
      SpreadsheetApp.getActiveSpreadsheet().toast('Searching folder ' + childFolder.getName() +
        ' at depth ' + searchDepth + " ...", 'Status', -1);
      
      // Get folder information
      // Logger.log("Folder Name: " + childFolder.getName());
      outputRows.push([
        parentFolderName + "/" + childFolder.getName(),
        childFolder.getName(),
        "Folder",
        childFolder.getDateCreated() ? childFolder.getDateCreated() : "NULL",
        childFolder.getUrl() ? childFolder.getUrl() : "NULL",
        childFolder.getLastUpdated() ? childFolder.getLastUpdated() : "NULL",
        childFolder.getDescription() ? childFolder.getDescription() : "NULL",
        childFolder.getSize() ? childFolder.getSize() : "NULL",
        childFolder.getOwner() ? childFolder.getOwner() : "NULL", 
        childFolder.getOwner().getEmail() ? childFolder.getOwner().getEmail() : "NULL",
        childFolder.getSharingPermission() ? childFolder.getSharingPermission() : "NULL",
        childFolder.getSharingAccess() ? childFolder.getSharingAccess() : "NULL"
        //, '=HYPERLINK("' + childFile.getUrl() + '", IMAGE("' + Drive.Files.get(childFolder.getId()).thumbnailLink + '",1))' //The 'Drive service' is a G-Suite service (commercial service)
        // more info: https://developers.google.com/apps-script/reference/drive/folder 
      ]);
      
      // cache outputs
      setCache_(outputRows, lockWaitTime, cacheTimeout);
      
      // List files inside the folder
      outputRows = getChildFiles_(
        parentFolder, childFolder, listFiles, cacheTimeout, outputRows);
      
      // Recursive call of the current sub-folder
      outputRows = getChildFolders_(searchDepth++, parentFolderName + "/" + childFolder.getName(), 
        childFolder, sheet, listFiles, cacheTimeout, lockWaitTime, outputRows, searchDepthMax);
    }
  } catch (e) {
    Logger.log('Timed out: Restarting! ' + e.toString());
    SpreadsheetApp.getActiveSpreadsheet().toast( 'Timed out!', 'Status', -1);
  }
  
  // cache outputs
  setCache_(outputRows, lockWaitTime, cacheTimeout);
  
  return outputRows;
}


// ===========================================================================================================
// Get the list of files in the selected folder
function getChildFiles_(parentFolder, childFolder, listFiles, cacheTimeout, outputRows) {
  var childFiles = childFolder.getFiles();
  var childFile = null;
  var path = ""
  
  try{
    // List files inside the folder
    while (listFiles && childFiles.hasNext()) {
      childFile = childFiles.next();
      
      // derive path
      if (parentFolder === null){
        path = childFolder.getName() + "/" + childFile.getName()
      }else{
        path = parentFolder.getName() + "/" + childFolder.getName() + "/" + childFile.getName()
      }
      
      // Get file information
      //Logger.log("File Name: " + childFile.getName());
      outputRows.push([
        path,
        childFile.getName(),
        childFile.getName().split('.').pop(),
        childFile.getDateCreated() ? childFile.getDateCreated() : "NULL",
        childFile.getUrl() ? childFile.getUrl() : "NULL",
        childFile.getLastUpdated() ? childFile.getDescription() : "NULL",
        childFile.getDescription() ? childFile.getDescription() : "NULL",
        childFile.getSize() ? childFile.getSize() : "NULL", 
        childFile.getOwner() ? childFolder.getOwner() : "NULL",
        childFile.getOwner().getEmail() ? childFile.getOwner().getEmail() : "NULL",
        childFile.getSharingPermission() ? childFile.getSharingPermission() : "NULL",
        childFile.getSharingAccess() ? childFile.getSharingAccess() : "NULL" 
        //, '=HYPERLINK("' + childFile.getUrl() + '", IMAGE("' + Drive.Files.get(childFile.getId()).thumbnailLink + '",1))' //The 'Drive service' is a G-Suite service (commercial service)
        // more info: https://developers.google.com/apps-script/reference/drive/folder 
      ]);
    }

    // cache outputs
    setCache_(outputRows, lockWaitTime, cacheTimeout);
  } catch (e) {
    Logger.log('Timed out: Restarting! ' + e.toString());
    SpreadsheetApp.getActiveSpreadsheet().toast('Timed out!', 'Status', -1);
  }
  return outputRows;
}


// ===========================================================================================================
// Set the values from cache
function setCache_(outputRows, lockWaitTime, cacheTimeout) {
  try{
    var cache = CacheService.getScriptCache();
    var lock = LockService.getScriptLock();
    
    lock.waitLock(lockWaitTime);
    cache.put(cacheOutputs, JSON.stringify(outputRows), cacheTimeout);
    lock.releaseLock();
  } catch (e) {
    Logger.log('Timed out: Restarting! ' + e.toString());
    SpreadsheetApp.getActiveSpreadsheet().toast('Timed out!', 'Status', -1);
  }
}


// ===========================================================================================================
// Get the values in cache
function getCache_(lockWaitTime) {
  try{
    var outputRows = [];
    var cache = CacheService.getScriptCache();
    var lock = LockService.getScriptLock();
    
    lock.waitLock(lockWaitTime);
    outputRows =  JSON.parse(cache.get(cacheOutputs));
    if (outputRows === undefined || outputRows === null ||
        outputRows[0] === undefined || outputRows[0] === null) {
      outputRows = JSON.parse(cache.get(cacheOutputs));
    }
    lock.releaseLock();
  } catch (e) {
    SpreadsheetApp.getActiveSpreadsheet().toast('Timed out!', 'Status', -1);
  }
  return outputRows;
}


// ===========================================================================================================
// Write outputs to the selected spreadsheet
function writeOutputs_(sheet, outputRows, appendToSheet) {
  try{
    var range, rowStart, indexStart, indexEnd = null;
    var headerRow = ["Full Path", "Name", "Type", "Date", "URL", "Last Updated", "Description", "Size",
                     "Owner", "Sharing Permission", "Sharing Access"]; //, "Thumbnail"];
    SpreadsheetApp.getActiveSpreadsheet().toast('Writing outputs...', 'Status', -1);
    
    if (sheet !== null && outputRows.length > 0) {
      if (appendToSheet === false) {
        sheet.getRange('A3:L').clear();
        sheet.appendRow(headerRow);
        rowStart = 4;
      } else {
        rowStart = getRowsFilled_(sheet, "A1:A") + 1;
      }
      
      indexStart = 0;
      indexEnd = Math.min(writeBatchSize, outputRows.length);
      
      while (indexStart < outputRows.length) {
        range = sheet.getRange(rowStart + indexStart, 1, indexEnd - indexStart, outputRows[0].length);
        range.setValues(outputRows.slice(indexStart, indexEnd));
        a = outputRows.slice(indexStart, indexEnd);
        
        indexStart = indexEnd;
        indexEnd =  Math.min(indexStart + writeBatchSize, outputRows.length);
      }
      
      range = sheet.getRange(getRowsFilled_(sheet, "A1:A") + 1, 1, 1, 1);
      range.setValues([["End of List!"]]);
    }
    
  } catch (e) {
    SpreadsheetApp.getActiveSpreadsheet().toast('Timed out!', 'Status', -1);
  }
}


// ===========================================================================================================
// Get number of rows filled in the selected spreadsheet
function getRowsFilled_(sheet, selectedRange) {
  var selectedMatrix = sheet.getRange(selectedRange).getValues();
  return selectedMatrix.filter(String).length;
}


// ===========================================================================================================
// Delete the global cache
function deleteCache_() {
  try{
    var cache = CacheService.getScriptCache();
    var lock = LockService.getScriptLock();
    
    lock.waitLock(this.lockWaitTime);
    cache = CacheService.getScriptCache();
    cache.remove(cacheOutputs);
    lock.releaseLock();
  } catch (e) {
    Logger.log('Failed to delete cache! ' + e.toString());
    SpreadsheetApp.getActiveSpreadsheet().toast('Failed to delete cache! Try again in a few minutes.');
  }
}


// ===========================================================================================================
// Delete triggers
function deleteTriggers_() {
  var triggers = ScriptApp.getProjectTriggers();
  try{
    for (var i = 0; i < triggers.length; i++) {
      if (triggers[i].getHandlerFunction() === "run") {
        ScriptApp.deleteTrigger(triggers[i]);
      }
    }
  } catch (e) {
    Logger.log('Failed to delete triggers! ' + e.toString());
    SpreadsheetApp.getActiveSpreadsheet().toast('Failed to delete triggers! Try again in a few minutes.');
  }
}


// ===========================================================================================================
// Set kill flag
function setKillFlag_(state, cacheTimeout) {
  var lock = LockService.getScriptLock();
  try{
    lock.waitLock(this.lockWaitTime);
    cache = CacheService.getScriptCache();
    cache.put(cacheKillFlag, state, cacheTimeout);
    lock.releaseLock();
  } catch (e) {
    SpreadsheetApp.getActiveSpreadsheet().toast('Failed to set kill flag! Try again in a few minutes.');
  }
}


// ===========================================================================================================
// Get kill flag
function getKillFlag_() {
  killFlag = false;
  try {
    cache = CacheService.getScriptCache();
    //lock.waitLock(this.lockWaitTime);
    killFlag = cache.get(cacheKillFlag) === 'true';
    //lock.releaseLock();
  } catch (e) {
    SpreadsheetApp.getActiveSpreadsheet().toast('Failed to set kill flag! Try again in a few minutes.');
  }
  return killFlag;
}

해당 코드 블록으로 넣어준 뒤 ‘좌측 상단’에서 저장버튼을 눌러 세이브해주세요.

앱스크립트 승인하기 [권한 검토 -> 고급 -> 해당 프로젝트로 이동(안전하지 않음)]

이후 생기는 실행 버튼을 눌러 진행해줍니다.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 앱스크립트 승인하기 [권한 검토 -> 고급 -> 해당 프로젝트로 이동(안전하지 않음)]


버튼을 누르고 나면 아래처럼 승인 필요에서 ‘권한 검토’로 진행해주시고 구글 계정으로 로그인하신 후 계속해주시면 됩니다.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 앱스크립트 승인하기 [권한 검토 -> 고급 -> 해당 프로젝트로 이동(안전하지 않음)]

진행하고 나면 무섭게 아래 같은 화면이 뜨실 텐데요 가볍게 ‘고급’ 버튼을 누른  후

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 앱스크립트 승인하기 [권한 검토 -> 고급 -> 해당 프로젝트로 이동(안전하지 않음)]

해당 프로젝트로 이동(안전하지 않음)을 눌러주세요.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 앱스크립트 승인하기 [권한 검토 -> 고급 -> 해당 프로젝트로 이동(안전하지 않음)]

여기까지 따라오셨다면 허용을 눌러 앱스 크립트 부분을 마무리 지어줍니다.

이제 저희는 A1셀 에 드라이브 주소중 ID값만 추출해서 넣어주시면 되는데요. 스크립트를 편하게 실행하기 위해서 실행 버튼을 만들 차례입니다.

인덱스 실행 버튼 만들기

‘삽입’ – ‘그림’

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 인덱스 실행 버튼 만들기
구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 인덱스 실행 버튼 만들기

텍스트 필드를 활용해서 조그마한 버튼을 만드신 후 “저장 후 닫기”를 눌러주면 됩니다.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 인덱스 실행 버튼 만들기

꾸미지 않으면 이런 식으로 버튼 이미지가 들어가게 될 거예요… (무식해 보인다..)

이제 해당 이미지를 한번 눌러보면 ⁝ 버튼이 보일 텐데 눌러보자. 스크립트 할당 이란 녀석을 추가로 눌러주면 됩니다.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 인덱스 실행 버튼 만들기

우리는 복잡할 필요 없이 run 이라고만 적어주면 완성!

그리고 버튼을 누르면 아래와 같은 느낌으로 사용해보실 수 있습니다.

구글스프레드 시트 자동화 [1] 문서 인덱스 만들기 - undefined - undefined - 인덱스 실행 버튼 만들기

이러한 형태로 필요할 때마다 버튼을 눌러서 갱신할 수도 있고, 혹은 특정 시간, 일을 기점으로 갱신할 수 도 있습니다.
이렇게 링크를 빠르게 가져올 수 있는 형태로 한 번만 만들어두면 어디에서든 필요한 링크 내 내용을 가져올 수 있는 함수인

IMPORTRANGE 함수와 강력한 시너지를 내게 됩니다.

따라 하기 어려우신 분들은 댓글로 이메일 주소를 남겨주시면 이메일로 문서 Copy 링크를 보내드리도록 하겠습니다 🙂

요청사항 관리가 어려워져서 아래 링크에서 구매해주시면 가이드와 함께 링크 동봉 드리고 있습니다!
[1600원 / 첫 구매 할인쿠폰 – 1000원 : 600원 입니다 :)]

https://smartstore.naver.com/zeey/products/8178724195

급상여 명세서 PDF파일 CSV로 변환하기 (테이블 구조 Tabula-py)
구글 스프레드시트로 대출 상환액 계산하기 (PMT, PPMT, IPMT)

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다