수집과 작성까지만 하면 100점, 복습을 통한 진정한 내재화까지 하면 100억!!

  • 목적 및 배경

    • n8n 계정 받기 !!

    • 옵시디언에 뭔가를 기록하고 수집하고는 있는데, 복습이나 다시 보기를 하고 있지 않고 있어 뭔가 쌓이는 느낌이 없음

    • 복습을 강제화 할 도구나 장치를 필요함

      매 1시간 단위로 옵시디언에 새로 추가된 새 글들을 검색하여 요약본 및 체크리스트 만들기

    사용 도구

    • 옵시디언 (구글 드라이브와 연결되어 있음)

    • 구글 드라이브

    • 구글 앱 스크립트

    • OpenAI 4o API

    실행

    • 구글 드라이브에서 옵시디언 볼트에 해당하는 디렉토리에 권한 주기

    • Gemini에게 구글 앱 스크립트 생성 요청

    • 앱 스크립트 생성 및 트리거 설정

    • 샘플 글 생성 및 요약본 확인

    1. 구글 드라이브에서 옵시디언 볼트에 해당하는 디렉토리에 권한 주기

    • 작업 대상 디렉토리 주소 확인

    • 작업 예외 대상 디렉토리 주소 확인

    권한주기

    • 옵시디언 디렉토리는 나만 보기 때문에 별도의 권한은 주지 않음.

    • 혹, 다른 사람과 협업이 필요할 경우 아래와 같이 설정하면 됨.

    - 드라이브 상단 디렉토리 경로 표시 부분 맨 우측 (i) 버튼 클릭
    - 액세스 관리 클릭
    - 일반 액세스 : 제한됨 --> **링크가 있는 모든 사용자**
    -            뷰어 --> **편집자**(파일정리, 추가 및 수정) 
    - 완료 클릭
    

    2. Gemini에게 구글 앱 스크립트 생성 요청

    구글 앱스크립트 코드를 아래 규격에 맞게 작성해 줘.
    ---
    1. 검색 대상 디렉토리 : https://drive.google.com/drive/u/0/folders/XXXXXX 및 하위 디렉토리
    2. 검색 예외 디렉토리 : https://drive.google.com/drive/u/0/folders/YYYYYY
    3. 이전 마지막 트리거 시간과 현재 트리거 실행 시간 사이에 하위 디렉토리에 신규로 생성된 .md 파일들을 검색해 줘.
    4. 이전 마지막 트리거 시간이 없으면 현재 트리거 실행 시간보다 1시간 이전으로 설정해 줘.
    5. 검색된 파일들들의 내용을 요약 및 정리해줘. 이때 open ai 4o api를 사용해서 수행해 줘. 
    6. api key는 내가 수동으로 변경할 때, 소스코드에 잘 찾을 수 있게 주석 넣어줘.
    7. 요약 및 정리된 결과들은 마크다운 문법으로 "검색예외 디렉토리 하위"에 [오늘날짜]_[실행시간]_요약.md 하나의 파일로 만들어줘.
    8. 검색된 문서 별 요약 포맷
       ## 글제목(옵시디언 링크)
       ### 주용 내용
       ### 더 알아봐야할 것 3가지 추천
    9. 생성하는 md 파일의 최상단에, 문서 제목별 다시 학습여부(체크박스)를 표 형태로 정리해줘.
    10. 실행 후 현재 트리거 실행시간을 이 스크립트의 마지막 트리거 시간으로 사용할 수 있게 관리(저장)해 줘 
    

    결과

    /**
     * @OnlyCurrentDoc
     *
     * This Google Apps Script automates the process of finding new markdown files in Google Drive,
     * summarizing their content using the OpenAI API, and creating a consolidated summary file.
     */
    
    // --- CONFIGURATION ---
    // 1. OpenAI API 키를 이곳에 입력하세요.
    //    (Enter your OpenAI API key here.)
    const OPENAI_API_KEY = "YOUR_OPENAI_API_KEY";
    
    // 2. 검색을 시작할 최상위 폴더의 ID를 입력하세요.
    //    (Enter the ID of the root folder to search.)
    const SEARCH_ROOT_FOLDER_ID = "XXXXXX";
    
    // 3. 검색에서 제외하고, 결과 파일을 저장할 폴더의 ID를 입력하세요.
    //    (Enter the ID of the folder to exclude from the search and save the result file.)
    const EXCEPTION_AND_OUTPUT_FOLDER_ID = "YYYYYY";
    
    /**
     * 메인 함수: 스크립트의 전체 실행을 조율합니다.
     * Main function: Orchestrates the entire script execution.
     */
    function main() {
      const currentTime = new Date();
      const lastExecutionTime = getLastExecutionTime(currentTime);
    
      console.log(`검색 범위: ${lastExecutionTime.toLocaleString()} 부터 ${currentTime.toLocaleString()} 까지`);
    
      const rootFolder = DriveApp.getFolderById(SEARCH_ROOT_FOLDER_ID);
      const newFiles = findNewMarkdownFiles(rootFolder, lastExecutionTime, currentTime);
    
      if (newFiles.length === 0) {
        console.log("새로 생성된 .md 파일이 없습니다.");
        return;
      }
    
      console.log(`요약 대상 파일 ${newFiles.length}개 발견: ${newFiles.map(f => f.getName()).join(', ')}`);
    
      const summaryContent = generateSummary(newFiles);
    
      if (summaryContent) {
        createSummaryFile(summaryContent, currentTime);
        saveCurrentExecutionTime(currentTime);
        console.log("요약 파일 생성을 완료했습니다.");
      } else {
        console.log("파일 요약 중 오류가 발생했습니다.");
      }
    }
    
    /**
     * 지정된 폴더와 하위 폴더에서 특정 기간 동안 생성된 새 마크다운 파일을 재귀적으로 찾습니다.
     * Recursively finds new markdown files created within a specific period in a given folder and its subfolders.
     * @param {GoogleAppsScript.Drive.Folder} folder - 검색할 폴더 객체. (The folder object to search.)
     * @param {Date} startTime - 검색 시작 시간. (The start time for the search.)
     * @param {Date} endTime - 검색 종료 시간. (The end time for the search.)
     * @returns {GoogleAppsScript.Drive.File[]} 새로 찾은 파일의 배열. (An array of newly found files.)
     */
    function findNewMarkdownFiles(folder, startTime, endTime) {
      let filesFound = [];
      const foldersToSearch = [folder];
      const searchedFolderIds = new Set([EXCEPTION_AND_OUTPUT_FOLDER_ID]); // 처음부터 예외 폴더는 제외
    
      while (foldersToSearch.length > 0) {
        const currentFolder = foldersToSearch.pop();
        searchedFolderIds.add(currentFolder.getId());
        
        // 파일 검색
        const files = currentFolder.getFiles();
        while (files.hasNext()) {
          const file = files.next();
          const fileCreationTime = file.getDateCreated();
          if (file.getName().toLowerCase().endsWith('.md') && fileCreationTime >= startTime && fileCreationTime < endTime) {
            filesFound.push(file);
          }
        }
    
        // 하위 폴더 검색
        const subFolders = currentFolder.getFolders();
        while (subFolders.hasNext()) {
          const subFolder = subFolders.next();
          if (!searchedFolderIds.has(subFolder.getId())) {
            foldersToSearch.push(subFolder);
          }
        }
      }
      return filesFound;
    }
    
    /**
     * PropertiesService에서 마지막 스크립트 실행 시간을 가져옵니다. 없으면 1시간 전으로 설정합니다.
     * Gets the last script execution time from PropertiesService. If it doesn't exist, sets it to 1 hour ago.
     * @param {Date} currentTime - 현재 스크립트 실행 시간. (The current script execution time.)
     * @returns {Date} 마지막 실행 시간. (The last execution time.)
     */
    function getLastExecutionTime(currentTime) {
      const scriptProperties = PropertiesService.getScriptProperties();
      const lastExecutionTimeStr = scriptProperties.getProperty('lastExecutionTime');
    
      if (lastExecutionTimeStr) {
        return new Date(lastExecutionTimeStr);
      } else {
        // 1시간 전으로 설정
        return new Date(currentTime.getTime() - 60 * 60 * 1000);
      }
    }
    
    /**
     * 현재 실행 시간을 PropertiesService에 저장합니다.
     * Saves the current execution time to PropertiesService.
     * @param {Date} currentTime - 저장할 현재 시간. (The current time to save.)
     */
    function saveCurrentExecutionTime(currentTime) {
      const scriptProperties = PropertiesService.getScriptProperties();
      scriptProperties.setProperty('lastExecutionTime', currentTime.toISOString());
    }
    
    /**
     * 파일 목록을 받아 OpenAI API를 사용하여 내용을 요약하고 마크다운 형식으로 구성합니다.
     * Takes a list of files, summarizes their content using the OpenAI API, and formats it as markdown.
     * @param {GoogleAppsScript.Drive.File[]} files - 요약할 파일의 배열. (An array of files to summarize.)
     * @returns {string|null} 생성된 마크다운 내용 또는 오류 발생 시 null. (The generated markdown content, or null on error.)
     */
    function generateSummary(files) {
      const fileNames = files.map(file => file.getName());
    
      // 1. 다시 학습 여부 체크박스 테이블 생성
      let summaryTable = "| 다시 학습 여부 | 문서 제목 |\n";
      summaryTable += "| :---: | :--- |\n";
      fileNames.forEach(name => {
        summaryTable += `| [ ] | ${name.replace('.md', '')} |\n`;
      });
    
      let summaries = "";
      for (const file of files) {
        const fileName = file.getName();
        const fileContent = file.getBlob().getDataAsString('UTF-8');
        const obsidianLink = `obsidian://open?file=${encodeURIComponent(fileName)}`;
        
        const summaryPrompt = `
          다음 텍스트를 아래 형식에 맞춰 요약해줘. 각 항목은 한글로 작성해줘.
    
          ### 주요 내용
          - (여기에 텍스트의 핵심 내용을 요약)
    
          ### 더 알아봐야할 것 3가지 추천
          - (여기에 텍스트 내용과 관련하여 더 깊이 학습하거나 탐구할 만한 주제 3가지를 추천)
    
          ---
          텍스트:
          ${fileContent}
        `;
        
        try {
          const gptResponse = callOpenAI(summaryPrompt);
          summaries += `\n## [${fileName.replace('.md', '')}](${obsidianLink})\n`;
          summaries += gptResponse + '\n';
        } catch (e) {
          console.error(`'${fileName}' 파일 요약 중 오류 발생: ${e.toString()}`);
          summaries += `\n## [${fileName.replace('.md', '')}](${obsidianLink})\n`;
          summaries += `### 요약 실패\n- 오류가 발생하여 내용을 요약할 수 없었습니다.\n- 오류 내용: ${e.message}\n`;
        }
      }
    
      return summaryTable + summaries;
    }
    
    
    /**
     * OpenAI API를 호출하여 텍스트 요약을 요청합니다.
     * Calls the OpenAI API to request a text summary.
     * @param {string} prompt - API에 전송할 프롬프트. (The prompt to send to the API.)
     * @returns {string} API로부터 받은 요약 텍스트. (The summary text received from the API.)
     */
    function callOpenAI(prompt) {
      const API_URL = "https://api.openai.com/v1/chat/completions";
    
      const payload = {
        model: "gpt-4o",
        messages: [{
          role: "user",
          content: prompt
        }],
        temperature: 0.5,
      };
    
      const options = {
        method: "post",
        contentType: "application/json",
        headers: {
          Authorization: "Bearer " + OPENAI_API_KEY,
        },
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
      };
      
      const response = UrlFetchApp.fetch(API_URL, options);
      const responseCode = response.getResponseCode();
      const responseBody = response.getContentText();
    
      if (responseCode === 200) {
        const jsonResponse = JSON.parse(responseBody);
        return jsonResponse.choices[0].message.content.trim();
      } else {
        throw new Error(`OpenAI API Error: ${responseCode} - ${responseBody}`);
      }
    }
    
    /**
     * 요약된 내용을 바탕으로 최종 마크다운 파일을 생성합니다.
     * Creates the final markdown file based on the summarized content.
     * @param {string} content - 파일에 작성할 마크다운 내용. (The markdown content to write to the file.)
     * @param {Date} currentTime - 파일 이름 생성을 위한 현재 시간. (The current time for generating the filename.)
     */
    function createSummaryFile(content, currentTime) {
      const outputFolder = DriveApp.getFolderById(EXCEPTION_AND_OUTPUT_FOLDER_ID);
      const formattedDate = Utilities.formatDate(currentTime, "GMT+9", "yyyy-MM-dd_HH-mm-ss");
      const fileName = `${formattedDate}_요약.md`;
      
      outputFolder.createFile(fileName, content, MimeType.PLAIN_TEXT);
    }
    
    

    3. 앱 스크립트 생성 및 트리거 설정

    스크립트 생성

    • 드라이브 좌측 "+ 신규" 클릭

    • 더보기 클릭

    • Google Apps Script 클릭

    • 기존 내용 삭제

    • 위 스크립트 Copy&Paste

    • YOUR_OPENAI_API_KEY 문자열 찾아서, 실제 사용가능한 키로 변경해 주기

    • 좌측상단에서 "제목없는 프로젝트" 이름 변경 ---> "생성글_요약"

    트리거 설정

    • 좌측 패널에서 자명종 시계모양 트리거 클릭

    • 오른쪽 아랭세 트리거 추가 클릭

    • 함수명 확인 : main

    • 시간 간격 선텩 : 1시간마다

    • 하단 저장 클릭

      • 메일 주소 선택

      • Advanced 클릭

      • "생성글_요약" 클릭

      • Allow 클릭

    스크립트 테스트

    • 좌측 패널에서 <> 편집기 선택

    • 상단 "실행" 버튼 클릭

    • 실행중...

    • 한국과 중국어 단어 목록

    4. 샘플 글 생성 및 요약본 확인

    • 로컬 옵시디언에 새 글 2개 추가

    • 구글 드라이브에 새 글 2개 추가된거 확인

    • 스크립트 실행 또는 기다림..

    • 결과 확인

    • 한국 달력의 스�크린 샷
      한국어 앱의 스크린 샷


    • 로컬 옵시디언에서 요약본 확인

      한국어 텍스트가있는 검은 색 화면
    • 1시간 뒤 트리거 동작 확인!!

      • 시간(트리거) 기반과 편집기(수동) 으로 구분된 관리됨을 확인.

        스프레드 시트에서 숫자 ��목록의 스크린 샷

    느낀점

    • 로직을 좀 더 정교화 해야 함. 생성시간 보다는 수정시간으로 스크립트를 만들었으면 더 정확할 듯.

    • 수 많은 자동화 도구들 중 적재적소에 딱 맞는 도구를 이용하기 위해서는 여러가지 툴 이용해 봐야 함.

    • 사례글 많이 보고, 많이 듣고 인사이트를 넓혀야 함.

    다음 할 일

    • 에빙하우스의 망각곡선에 기반하여 n8n을 이용한 옵시디언 복습 자동화 시나리오(다양한 채널로 요약본 보내기) 만들고, 실제 구현 해 보기

    도움 받은 글

    • 리부티너님 사례글

    • 원샷님 사례글

    • 여행가J님 강의

6
10개의 답글

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요