챕터 7

정기 보고서 자동 생성 — 매주 요약해 PDF·메일로 보내기

매주 월요일 아침 지난주 실적을 요약해 PDF로 만들고 팀에 메일로 보내는 보고서를 앱스 스크립트와 시간 기반 트리거로 자동화합니다. 실행 시간 한도와 실패 알림 설정까지 다룹니다.

월요일 아침 보고서를 대신 만들어 두기

4장에서 시트 명단으로 메일을 보내 봤고, 5~6장에서는 파일을 이름 규칙대로 바꾸고 여러 파일을 하나로 합쳤습니다. 이번에는 이 조각들을 이어 붙입니다. 시트의 지난주 숫자를 집계하고, 표를 PDF로 뽑고, 메일에 붙여 보내는 일을 사람이 아무것도 누르지 않아도 매주 월요일에 돌아가게 만듭니다.

한 번 만들어 두면 가장 오래 쓰는 자동화이기도 합니다. 그래서 이번 장은 코드만큼 언제 돌고, 언제 멈추는지에 분량을 씁니다.

만들 것의 구조

매출 시트에는 A열 날짜, B열 담당자, C열 금액이 쌓입니다. 스크립트는 지난주 월요일부터 일요일까지의 행만 골라 담당자별로 더하고, 결과를 주간보고 시트에 적은 뒤, 그 시트 한 장만 PDF로 내보내 메일로 보냅니다.

단계 하는 일 쓰는 서비스
1 지난주 월~일 범위 계산 Date, Utilities.formatDate
2 담당자별 합계 집계 시트 값 읽기(getValues)
3 주간보고 시트에 쓰기 setValues, SpreadsheetApp.flush
4 해당 시트만 PDF로 변환 UrlFetchApp
5 첨부해 발송 MailApp
6 매주 월요일 자동 실행 시간 기반 트리거

요청 프롬프트

2장에서 익힌 다섯 요소(목적·입력·출력·예시·제약)를 그대로 채웁니다. Claude나 ChatGPT에 아래처럼 보내세요. 실제 매출 대신 가짜 예시를 넣었다는 점도 눈여겨보세요.

구글 앱스 스크립트를 써 주세요. 구글 시트의 '매출' 시트(1행은 제목)에 A열 날짜, B열 담당자, C열 금액이 있습니다. 예: 2026-09-21 / 김 / 100000, 2026-09-27 / 이 / 50000. 실행하면 지난주 월요일부터 일요일까지의 행만 담당자별로 합산해서 '주간보고' 시트(없으면 새로 만들고, 있으면 내용을 지우고 다시 씀)에 '담당자 / 매출' 표와 합계 행으로 적어 주세요. 그다음 '주간보고' 시트 한 장만 PDF로 만들어 team@example.com으로 메일 첨부 발송해 주세요. 제목은 '[자동] 2026-09-21 주 매출 보고' 형식입니다. 제약: 날짜 표시는 Asia/Seoul 기준으로, 매주 월요일 오전 8시대에 자동 실행되는 트리거를 만드는 함수도 따로 넣어 주세요. 초보자가 읽을 수 있게 주석을 달아 주세요.

보고서 스크립트 실행하기

완성 코드

아래 코드는 실제 시트 환경을 흉내 낸 모의 실행으로 동작을 확인한 것입니다. 시트 이름 매출·주간보고와 받는 사람 주소만 본인 것으로 바꾸면 됩니다.

// '매출' 시트: A열=날짜, B열=담당자, C열=금액
const TO = 'team@example.com';

function weeklyReport() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const rows = ss.getSheetByName('매출').getDataRange().getValues().slice(1);

  // 지난주 월요일 00:00 ~ 이번 주 월요일 00:00
  const today = new Date();
  const thisMonday = new Date(today.getFullYear(), today.getMonth(),
                              today.getDate() - ((today.getDay() + 6) % 7));
  const lastMonday = new Date(thisMonday.getTime() - 7 * 24 * 60 * 60 * 1000);

  const byPerson = {};
  let total = 0;
  rows.forEach(([date, person, amount]) => {
    if (!(date instanceof Date) || date < lastMonday || date >= thisMonday) return;
    byPerson[person] = (byPerson[person] || 0) + Number(amount);
    total += Number(amount);
  });

  let report = ss.getSheetByName('주간보고');
  if (!report) report = ss.insertSheet('주간보고');
  report.clearContents();
  const period = Utilities.formatDate(lastMonday, 'Asia/Seoul', 'yyyy-MM-dd') + ' 주';
  const table = [['담당자', '매출'], ...Object.entries(byPerson), ['합계', total]];
  report.getRange(1, 1).setValue(period + ' 매출 보고');
  report.getRange(3, 1, table.length, 2).setValues(table);
  SpreadsheetApp.flush();                       // 쓴 내용을 PDF 만들기 전에 반영

  const pdf = exportSheetAsPdf(ss, report).setName('주간보고_' + period + '.pdf');
  MailApp.sendEmail({
    to: TO,
    subject: '[자동] ' + period + ' 매출 보고',
    body: '합계 ' + total.toLocaleString('ko-KR') + '원입니다. 자세한 표는 첨부 PDF를 확인해 주세요.',
    attachments: [pdf],
  });
}

// 특정 시트 한 장만 PDF로 내보내기
function exportSheetAsPdf(ss, sheet) {
  const url = 'https://docs.google.com/spreadsheets/d/' + ss.getId() +
    '/export?format=pdf&gid=' + sheet.getSheetId() + '&portrait=true&gridlines=false';
  const res = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
  });
  return res.getBlob();
}

// 한 번만 실행: 매주 월요일 오전 8~9시 사이 자동 실행 등록
function createTrigger() {
  ScriptApp.newTrigger('weeklyReport')
    .timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(8).create();
}

핵심은 두 곳입니다. (today.getDay() + 6) % 7은 오늘이 월요일로부터 며칠째인지 구하는 계산입니다. 일요일(0)은 6, 월요일(1)은 0이 되니, 이번 주 월요일 0시를 기준선으로 삼고 거기서 7일을 빼면 지난주 월요일이 나옵니다. 그리고 date >= thisMonday로 이번 주 행을 걸러 냅니다.

PDF는 getAs가 아니라 시트의 내보내기 주소로 받습니다. 시트 전체가 아니라 gid로 지정한 한 장만 나오게 하려는 선택입니다. 이 주소의 파라미터는 구글이 공식 문서로 정리해 두지 않았고 커뮤니티에서 알려진 방식이므로, 용지 크기나 방향을 바꿀 때는 결과 PDF를 직접 열어 확인하세요.

실행 결과

시스템 날짜를 2026년 10월 2일(금)로 두고 돌린 결과입니다. 시트에는 지난주 행 3개(9월 21일 김 100,000원, 9월 25일 김 20,000원, 9월 27일 이 50,000원)와 일부러 넣은 범위 밖 행 2개(9월 28일 김 999원, 9월 20일 이 777원)를 넣었습니다.

주간보고 시트: 2026-09-21 주 매출 보고 / 김 120,000 / 이 50,000 / 합계 170,000
메일 제목: [자동] 2026-09-21 주 매출 보고
메일 본문: 합계 170,000원입니다. 자세한 표는 첨부 PDF를 확인해 주세요.
첨부 파일명: 주간보고_2026-09-21 주.pdf

이번 주 월요일(9월 28일)과 그 이전 일요일(9월 20일) 행은 합계에 들어가지 않았습니다. 경계 행을 일부러 넣어 보는 습관이 집계 스크립트에서는 가장 값싼 검증입니다.

일요일 밤 11시에 쓴 행도, 월요일 0시 1분에 쓴 행도 한 주 안에서 정확히 갈립니다. 범위 비교는 >=와 < 조합으로 쓰세요.

매주 자동으로 돌리기: 트리거

트리거 만들기

createTrigger 함수를 편집기에서 한 번만 실행하면 등록됩니다. 두 번 실행하면 같은 트리거가 둘 생겨 메일이 두 통 나가니, 등록 후에는 왼쪽 시계 아이콘(트리거) 화면에서 한 줄만 있는지 확인하세요. 화면에서 직접 '트리거 추가'로 만들어도 결과는 같습니다.

8시에 만들었는데 8시 12분에 도는 이유

시간 기반 트리거는 정확한 시각이 아니라 시간대를 지정합니다. 구글 공식 가이드는 "매일 오전 9시 트리거를 만들면 9시와 10시 사이 어느 때든 실행될 수 있다"고 설명합니다(Google for Developers). nearMinute(분)을 붙이면 지정한 분의 앞뒤 15분 안으로 범위가 좁아집니다.

시간 기반 트리거에서 atHour(8)은 오전 8시 정각이 아니라 8시~9시 사이 어느 시점을 뜻합니다.

그래서 9시 회의 자료처럼 마감이 빠듯하면 오전 7시대로 잡는 편이 안전합니다.

한도와 시간대

항목 개인(gmail.com) Google Workspace
1회 실행 시간 6분 6분
트리거 총 실행 시간(하루) 90분 6시간
메일 수신자(하루) 100명 1,500명

(Google for Developers 할당량 문서, 2026년 9월 3일 갱신 기준)

주간 보고서는 몇 초면 끝나니 한도와 거리가 멉니다. 문제는 시간대입니다. 스크립트 프로젝트에는 시간대 설정이 따로 있고, 시트에도 별도의 시간대가 있어서 둘이 다르면 날짜가 어긋납니다. 프로젝트 설정에서 appsscript.json 표시를 켜면 "timeZone": "Asia/Seoul"이 맞는지 직접 볼 수 있습니다.

왜 중요한지 계산해 보겠습니다. 한국 시간 월요일 오전 8시는 UTC로 일요일 밤 11시입니다. 스크립트 시간대가 UTC 계열로 되어 있으면 코드는 '오늘은 일요일'이라고 판단하고, 이번 주 월요일을 6일 전으로 잡습니다. 지난주 범위가 통째로 한 주 밀려 엉뚱한 주의 합계가 정상적인 형식으로 발송됩니다. 오류 메시지도 없으니 알아채기 어렵습니다. 시간대가 맞는지부터 확인하세요.

실패 알림 켜기

트리거 설정 화면에서 실패 알림을 켜 두면, 트리거 함수가 실패했을 때 구글이 메일로 알려 줍니다. 알림은 noreply-apps-scripts-notifications@google.com에서 오므로 스팸함에 들어가지 않게 주소를 허용해 두세요. 알림 주기 선택지의 정확한 한국어 이름은 화면 버전에 따라 다를 수 있으니, 즉시 알림에 가까운 항목을 고르면 됩니다. 실패 이력은 왼쪽 '실행 내역' 화면에서도 볼 수 있습니다.

오류 수정 대화

Exceeded maximum execution time

매출 행이 수만 개로 늘고 집계 방식이 느려지면, 어느 날 실패 알림이 이런 메시지와 함께 옵니다. 아래처럼 원문을 그대로 붙여 다시 물으세요.

아래 앱스 스크립트가 매주 돌다가 이런 오류가 났습니다. 행이 3만 개 정도입니다. Exceeded maximum execution time 한 번 실행이 6분을 넘어서 그런 것 같은데, 읽고 쓰는 방식을 바꿔서 줄여 주세요. 코드는 아래와 같습니다. (코드 붙여 넣기)

원인은 1회 실행 한도 6분입니다. 흔한 해법은 두 가지로 알려져 있습니다. 셀을 하나씩 읽고 쓰는 getValue·setValue 반복을 getValues·setValues 일괄 처리로 바꾸는 것(3장에서 쓴 방식)과, 그래도 부족하면 마지막으로 처리한 행 번호를 PropertiesService에 저장해 다음 실행에서 이어 가는 것입니다. 이번 예제는 이미 일괄 읽기와 한 번에 쓰기를 쓰고 있어서, 이 오류가 나면 시트 데이터가 정말 커졌는지부터 의심하세요. 이 메시지의 원문은 공식 문서가 아닌 커뮤니티 자료로 확인한 표기라, 실제 화면의 문구와 조금 다를 수 있습니다.

PDF가 제목만 있고 표가 비어 있을 때

두 번째 사례는 메일은 갔는데 첨부 PDF를 열면 표가 비어 있는 경우입니다.

앱스 스크립트로 보낸 PDF가 제목 한 줄만 있고 담당자별 표가 비어 있습니다. 시트를 열어 보면 값은 들어가 있어요. 코드는 아래와 같습니다. 원인과 고친 코드를 알려 주세요.

PDF 변환은 스크립트 안에서 계산하는 것이 아니라 구글 서버가 시트를 읽어 만듭니다. 코드가 쓴 값이 아직 반영되기 전에 내보내면 쓰기 전 상태가 찍힐 수 있습니다. 그래서 예제에는 setValues 바로 뒤에 SpreadsheetApp.flush()를 넣어 두었습니다. 수정 대화에서 AI가 이 줄을 빼먹은 코드를 주거나, 코드를 정리하면서 지우면 이 증상이 나올 수 있으니 PDF가 이상하면 이 줄이 남아 있는지 먼저 보세요.

같은 계열로, 요청 헤더의 Authorization을 빠뜨리면 PDF 대신 로그인 페이지가 돌아온다는 경험담이 많습니다. 처음 실행할 때 권한 승인 창이 뜨는데, 거기서 외부 요청 권한을 허용했는지도 확인하세요.

보고서 자동화를 오래 굴리는 요령

세 가지만 지키면 반년 뒤에도 같은 메일이 옵니다.

  • 메일 수신자 수를 센다 받는 사람이 늘면 하루 한도(개인 100명, Workspace 1,500명)를 4장의 발송과 나눠 쓰게 됩니다.
  • 첫 달은 본인에게만 보낸다 TO를 내 주소로 두고 서너 주 숫자를 눈으로 맞춰 본 뒤 팀 주소로 바꾸세요.
  • 쓰는 계정을 확인한다 트리거는 만든 사람 계정으로 실행됩니다. 그 사람이 퇴사해 계정이 정지되면 예약 실행도 멈출 수 있으니, 이 이야기는 8장 인수인계에서 이어 갑니다.

슬랙이나 디스코드 같은 메신저로 알리고 싶다면 UrlFetchApp으로 웹훅 주소에 글을 보내는 방식이 같은 골격입니다. 다만 웹훅 주소는 비밀번호처럼 취급해야 해서 코드를 공개 저장소에 올리지 마세요.

정기 보고서는 집계 범위 계산, 시트 한 장 PDF 내보내기, 트리거 등록 세 가지가 전부입니다. 시간대 확인과 실패 알림만 챙기면 월요일 아침의 이 작업은 사라집니다. 다음 챕터에서는 '막혔을 때와 오래 쓰는 법 — 오류 사전·보안·유지보수'를 다룹니다. 지금까지 나온 오류 메시지를 한곳에 모으고, AI에 데이터를 넘길 때의 원칙과 인수인계 방법을 정리합니다.