본문 바로가기
HRDI_AI/[인공지능] 노션 데이터베이스로 자료 정리하고 Google Apps Sc

AppScript 보고서 자동생성 프로젝트

by Toddler_AD 2025. 12. 10.
  • Notion Database input data

 

 

  • Config.gs
const NOTION_API_KEY = 'ntn_481631440693PrRe';
const NOTION_DATASOURCE_ID = '2c5b36bf-4185'; 
const NOTION_DATABASE_ID = '2c5b36bf418';
const DRIVE_FOLDER_ID = '1Mn_D7y2vXfi6jKkP00';
const SLIDE_TEMPLATE_ID = '1H_PsvGv14H9ljBEhnZ5FYQUf5';
const SHEET_TEMPLATE_ID = '1SCr3vo_vlEaUecrNiE0JTcZQt';
const NOTION_VERSION = '2025-09-03'

 

 

  • SlideReport.gs
function createSlideReport(notionData) {
  if (!notionData || notionData.length === 0) {
    Logger.log('노션 데이터가 없습니다.');
    return;
  }

  // 템플릿 복사 → 최종 결과 슬라이드
  const templateFile = DriveApp.getFileById(SLIDE_TEMPLATE_ID);
  const newFile = templateFile.makeCopy("심사평가표 결과_Slide");
  const presentation = SlidesApp.openById(newFile.getId());

  // 템플릿의 첫 번째 슬라이드를 기준 슬라이드로 사용
  const templateSlide = presentation.getSlides()[0];

  notionData.reverse().forEach((item, index) => {
    // 템플릿 슬라이드를 복제해서 뒤에 붙임
    let slide = templateSlide.duplicate(); // 맨 끝에 추가

    // 플레이스홀더 대체
    slide.replaceAllText('{{no}}', item.no);
    slide.replaceAllText('{{이름}}', item.name);
    slide.replaceAllText('{{팀명}}', item.teamName);
    slide.replaceAllText('{{평가일}}', item.date);
    slide.replaceAllText('{{창의성}}', item.score1);
    slide.replaceAllText('{{효과성}}', item.score2);
    slide.replaceAllText('{{구현완결성}}', item.score3);
    slide.replaceAllText('{{발표능력}}', item.score4);
    slide.replaceAllText('{{합계}}', item.sum);
    slide.replaceAllText('{{평가의견}}', item.opinion);
  });

  // 마지막에 원본 템플릿 슬라이드 삭제 (안 그러면 그대로 남음)
  templateSlide.remove();

  presentation.saveAndClose();
  Logger.log("Google 슬라이드 보고서가 생성되었습니다: " + newFile.getUrl());
  // return newFile.getUrl();
  return saveAsPdfInFolder(newFile)
}

 

 

  • SheetReport.gs
function createSheetReport(notionData) {
  if (!notionData || notionData.length === 0) {
    Logger.log('노션 데이터가 없습니다.');
    return;
  }

  const dbTitle = getDatabaseTitle();
  // 템플릿 복사 → 최종 결과 스프레드시트
  const templateFile = DriveApp.getFileById(SHEET_TEMPLATE_ID);
  const newFile = templateFile.makeCopy("김선기_심사평가표 결과(Spreadsheet)");
  const ss = SpreadsheetApp.openById(newFile.getId());

  // 첫 번째 시트 가져오기
  const sheet = ss.getSheets()[0];

  try {
    notionData.forEach((item, index) => {
      Logger.log(`데이터 ${index}행 ${JSON.stringify(item.name)} 사용자 처리`);
      const newSheetName = `평가표(${item.name})`;
      const newSheet = sheet.copyTo(ss);
      newSheet.setName(newSheetName); // 새로운 시트 이름 설정
      newSheet.getRange('C1').setValue(dbTitle);
      newSheet.getRange('C2').setValue(item.teamName);
      newSheet.getRange('C3').setValue(item.name);
      newSheet.getRange('E5').setValue(item.score1);
      newSheet.getRange('E6').setValue(item.score2);
      newSheet.getRange('E7').setValue(item.score3);
      newSheet.getRange('E8').setValue(item.score4);
      newSheet.getRange('E9').setValue(item.sum);
      newSheet.getRange('B10:E10').setValue(item.opinion);
      newSheet.getRange('A11:E11').setValue(item.date);
    });

	  // 요약 시트 생성
    const summarySheet = ss.insertSheet('점수요약');
    summarySheet.getRange(1, 1).setValue('이름');
    summarySheet.getRange(1, 2).setValue('점수');
    notionData.forEach((item, index) => {
      summarySheet.getRange(index + 2, 1).setValue(item.name);
      summarySheet.getRange(index + 2, 2).setValue(item.sum);
    })

	  // 세로 막대그래프 생성
    const chart = summarySheet.newChart()
          .setChartType(Charts.ChartType.COLUMN)
          .addRange(summarySheet.getRange(2, 1, notionData.length, 2)) // 헤더제외한범위
          .setPosition(1, 4, 0, 0) // 시트내위치(1행4열), (0, 0)은차트의픽셀오프셋(X, Y) 
          .setOption('title', '학생별점수')
          .setOption('hAxis', { title: '학생이름'}) // 가로축제목
          .setOption('vAxis', { title: '점수', minValue: 0 }) // 세로축제목
          .build();
    summarySheet.insertChart(chart)
	  
    // Excel 파일(.xlsx)로 변환
    const url = "https://docs.google.com/feeds/download/spreadsheets/Export?key=" + newFile.getId() + "&exportFormat=xlsx";

    const token = ScriptApp.getOAuthToken();
    const response = UrlFetchApp.fetch(url, {
      headers: {
        'Authorization': 'Bearer ' + token
      }
    });

    const blob = response.getBlob().setName("김선기_심사평가표 결과.xlsx");

	  // 이메일 발송
    MailApp.sendEmail({
      to: "xippaz@gmail.com",
      subject: "심사평가표 결과 파일",
      body: "첨부된 Excel 파일을 확인해주세요.",
      attachments: [blob]
    });
    Logger.log('엑셀 파일이 메일로 전송되었습니다.')
  } catch(e) {
    Logger.log('오류 발생: ' + e.toString());
  }
  Logger.log("Google 슬라이드 보고서가 생성되었습니다: " + newFile.getUrl());
  
  return saveAsPdfInFolder(newFile)
}

 

 

  • PdfReport.gs
function saveAsPdfInFolder(newFile) {
    const pdfBlob = newFile.getAs("application/pdf");

    // 지정한 폴더(ID) 불러오기
    const parentFolder = DriveApp.getFolderById(DRIVE_FOLDER_ID);

    // "pdf" 하위 폴더 찾기 (없으면 생성)
    let pdfFolder;
    const subFolders = parentFolder.getFoldersByName("pdf");
    if (subFolders.hasNext()) {
        pdfFolder = subFolders.next(); // 이미 있으면 그대로 사용
    } else {
        pdfFolder = parentFolder.createFolder("pdf"); // 없으면 새로 생성
    }

    // pdf 폴더 안에 PDF 저장
    const pdfFile = pdfFolder.createFile(pdfBlob).setName(newFile.getName() + ".pdf");

    Logger.log("PDF 파일이 생성되었습니다: " + pdfFile.getUrl());
    return pdfFile.getUrl();
}

 

 

  • Code.gs
function main() {
  Logger.log(getDatabaseTitle());
  notionData = getNotionData();
  notionData.forEach((item, index) => {
    Logger.log(`${item.no}, ${item.name}, ${item.teamName}, ${item.topic}, ${item.sum}, ${item.opinion}`);
  });
  createSlideReport(notionData);
  createSheetReport(notionData);
}

function getNotionData() {
	const url = `https://api.notion.com/v1/data_sources/${NOTION_DATASOURCE_ID}/query`;
	const payload = { // 필요한 경우 필터링 및 정렬 조건 추가
		sorts: [
			{
				property: 'No',
				direction: 'ascending'
			}
		]
	}
	const options = {
		'method': 'post',
		'headers': {
			'Authorization': `Bearer ${NOTION_API_KEY}`,
			'Notion-Version': NOTION_VERSION, // Notion API 버전
			'Content-Type': 'application/json'
		},
		'payload': JSON.stringify(payload)
	};

	try {
		const response = UrlFetchApp.fetch(url, options);
		const data = JSON.parse(response.getContentText());
		return data.results.map(page => ({
      no: page.properties['No']?.title[0]?.text.content ?? '',
      name: page.properties['이름']?.rich_text[0]?.text.content ?? '',
      teamName: page.properties['팀명']?.select?.name ?? '',
      topic: page.properties['주제']?.rich_text[0]?.text.content ?? '',
      date: page.properties['평가일'].date?.start,
      score1: page.properties['창의성']?.number ?? 0,
      score2: page.properties['효과성']?.number ?? 0,
      score3: page.properties['구현완결성']?.number ?? 0,
      score4: page.properties['발표능력']?.number ?? 0,
      sum: page.properties['합계']?.formula?.number ?? 0,
      opinion: page.properties['평가의견'].rich_text[0]?.text.content ?? '',
    }))
	} catch (e) {
		Logger.log('노션 데이터 가져오기 오류: ' + e.toString());
		return null;
	}
}

function getDatabaseTitle() {
  const url = `https://api.notion.com/v1/databases/${NOTION_DATABASE_ID}`;
  const response = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: {
      'Authorization': `Bearer ${NOTION_API_KEY}`,
      'Notion-Version': NOTION_VERSION
    }
  });

  const json = JSON.parse(response.getContentText());
  const title = json.title?.[0]?.plain_text ?? '프로젝트_평가표';
  // Logger.log(json);
  return title;
}

 

 

  • 실행 Log
PM 9:19:02	알림	실행이 시작됨
PM 9:19:04	정보	251210 프로젝트 평가표(API 연결)
PM 9:19:04	정보	1, 김서현, 나자바바, 친환경 전기자전거 공유 시스템, 90, 매우 창의적이며 구현도 뛰어나 실현 가능성이 높습니다. 발표도 안정적이었습니다.
PM 9:19:04	정보	2, 이준영, CMIUC, AI 기반 독서 습관 분석 앱, 74, 창의성은 우수하나 구현과 효과성 면에서 다소 미흡했습니다. 발표는 양호합니다.
PM 9:19:04	정보	3, 박지민, 나자바바, 폐플라스틱 자동 분류 로봇, 97, 매우 창의적이고 완성도 높은 발표였습니다. 실현 가능성도 매우 큽니다.
PM 9:19:04	정보	4, 최민재, CMIUC, 실시간 버스 혼잡도 예측 서비스, 67, 아이디어는 좋았으나 발표와 효과성 측면에서 보완이 필요합니다.
PM 9:19:04	정보	5, 정유나, CMIUC, 시니어 맞춤형 건강 코칭 앱, 83, 안정적인 발표였고, 주제도 사회적으로 유의미합니다. 구현도 충실합니다.
PM 9:19:04	정보	6, 한도현, 나자바바, 폐건전지 수거 게임 플랫폼, 88, 재미와 교육을 결합한 참신한 시도입니다. 발표력도 인상적이었습니다.
PM 9:19:04	정보	7, 송하진, HELP, 감정 인식 기반 상담 챗봇, 74, 흥미로운 주제지만 구현 완성도와 발표력이 다소 부족했습니다.
PM 9:19:04	정보	8, 김도윤, HELP, 비대면 봉사활동 매칭 플랫폼, 92, 실용적이고 공익성이 높은 주제입니다. 전반적으로 완성도가 높았습니다.
PM 9:19:04	정보	9, 오세아, HELP, 재난 대비 훈련 시뮬레이터, 83, 발표가 명확했고 효과적인 구현을 보여주었습니다. 주제도 적절합니다.
PM 9:19:04	정보	10, 배지우, 나자바바, 어린이 코딩 교육용 게임, 99, 매우 완성도 높은 발표와 창의성. 교육적 효과도 높습니다. 훌륭한 결과물입니다.
PM 9:19:07	정보	Google 슬라이드 보고서가 생성되었습니다: https://docs.google.com/presentation/d/1F5Wny8GtUrTI3g3Dws5ctV1glSSwSJ5dqjNBws_43sM/edit?usp=drivesdk
PM 9:19:09	정보	PDF 파일이 생성되었습니다: https://drive.google.com/file/d/1x71YBxf10iPn0NFXV1lL1HqqMBqlNHWE/view?usp=drivesdk
PM 9:19:12	정보	데이터 0행 "배지우" 사용자 처리
PM 9:19:12	정보	데이터 1행 "오세아" 사용자 처리
PM 9:19:13	정보	데이터 2행 "김도윤" 사용자 처리
PM 9:19:14	정보	데이터 3행 "송하진" 사용자 처리
PM 9:19:15	정보	데이터 4행 "한도현" 사용자 처리
PM 9:19:15	정보	데이터 5행 "정유나" 사용자 처리
PM 9:19:16	정보	데이터 6행 "최민재" 사용자 처리
PM 9:19:17	정보	데이터 7행 "박지민" 사용자 처리
PM 9:19:18	정보	데이터 8행 "이준영" 사용자 처리
PM 9:19:19	정보	데이터 9행 "김서현" 사용자 처리
PM 9:19:21	정보	엑셀 파일이 메일로 전송되었습니다.
PM 9:19:21	정보	Google 슬라이드 보고서가 생성되었습니다: https://docs.google.com/spreadsheets/d/1VUUY1M_ONegYSR8kOA81wdMGWtPJd58dlBGmq06i1Xw/edit?usp=drivesdk
PM 9:19:25	정보	PDF 파일이 생성되었습니다: https://drive.google.com/file/d/1-WXi40zAz8a6-495rWmVJWgQFVziixn5/view?usp=drivesdk
PM 9:19:26	알림	실행이 완료됨

 

 

  • 요약시트 및 막대그래프

 

 

  • Spreadsheet 자동생성 보고서

 

  • Presentation 자동생성 보고서