3-2.실습: 업무 자동화 - Apps Script

실습 예제 #1: 예약 관리 자동화 (Google Forms → Google Calendar + 이메일)

🎯 실습 배경

작은 비즈니스나 개인 프로젝트에서 고객 예약을 효율적으로 관리하려면, 매번 직접 캘린더에 입력하고 확인 메일을 보내는 일이 번거롭습니다.
이번 실습에서는 Google Apps Script를 활용해, 다음과 같은 자동화를 직접 만들어보겠습니다.
고객은 Google Forms를 통해 예약 신청
예약 신청 내용은 Google Sheets에 자동 저장
예약이 접수되면 Google Calendar 일정 자동 등록
동시에 신청자에게 예약 확인 이메일(Google Gmail) 발송
즉, 한 번의 제출로 예약 관리 전 과정을 자동화하는 실습

📝 단계별 요구사항

1. Google Forms 만들기

아래 항목을 포함한 Google Forms를 하나 생성하세요:
고객 이름 (예: 홍길동)
고객 이메일 (예: test@example.com)
고객 전화번호 (예: 010-0000-0000)
예약 희망 날짜 (예: YYYY-MM-DD 형식)
각 질문에는 간단한 설명을 달아, 누구나 쉽게 입력할 수 있도록 합니다.

2. Google Sheets 연결

Forms 응답은 자동으로 연결된 Google Sheets에 저장됩니다.
응답 시트에는 예시 데이터가 포함되어 있어, 어떤 형태로 데이터가 쌓이는지 확인할 수 있습니다.

3. Google Calendar 이벤트 생성

폼 제출 시 응답 데이터를 읽어, 내 Google Calendar에 일정이 자동으로 추가됩니다.
기본 일정 시간은 예약 희망 날짜 기준 1시간으로 설정합니다.

4. 이메일 발송 (HTML 형식)

예약이 등록되면, 신청자의 이메일로 예약 확인 메일을 보냅니다.
이메일 본문은 HTML 형식으로 작성해, 예약 정보 요약(이름, 날짜 등)을 포함합니다.
추가로 캘린더 초대 버튼 모양을 삽입해, 시각적으로 더 보기 좋게 만듭니다.
(교육용이므로 실제 버튼 클릭은 작동하지 않아도 됩니다.)

5. 전체 자동화 실행

Google Apps Script에서 코드를 작성해, 폼 제출 이벤트(trigger)가 발생할 때 자동으로
구글 시트에 예약 신청 내용 입력
캘린더 일정 추가
이메일 발송
=⇒ 이 세 가지가 실행되도록 합니다.
쉽게 이해할 수 있도록, 코드에는 단계별 주석을 충분히 작성합니다.

📌 최종 목표

이 실습을 통해 "Apps Script로 업무 자동화"이 기본적 개념 학습

1. 프롬프트

# 실습 예제: 예약 자동화 시스템 (Google Forms → Google Sheets → Google Calendar + 이메일) ## 🎯 시나리오 나는 작은 비즈니스를 운영하고 있으며, 고객 예약을 매번 수기로 관리하기가 번거롭다. 이를 해결하기 위해, 고객이 Google Forms를 통해 예약을 신청하면 다음과 같은 자동화가 이루어지도록 만들고 싶다: - Google Forms에 입력된 응답이 Google Sheets에 저장되고, - 응답을 기반으로 Google Calendar에 예약 일정이 자동 등록되며, - 동시에 고객에게 예약 확인 이메일이 발송되는 시스템. 이 과정을 통해 **Forms, Sheets, Calendar, Gmail**을 연계한 업무 자동화를 이루고자 한다. --- ## 📝 단계별 요구사항 (초보자용 안내 포함) 1. **Google Forms 생성** - 응답 항목은 다음 세 가지를 포함해야 한다: * 고객 이름 (예: 홍길동) * 고객 이메일 (예: test@example.com) * 예약 희망 날짜 (예: YYYY-MM-DD 형식) - 각 질문에는 간단한 설명을 추가하여, 처음 사용하는 사람도 쉽게 이해할 수 있도록 한다. 2. **Google Sheets 연결** - 위 Form의 응답을 저장할 Google Sheets를 자동 생성한다. - 예시 응답(샘플 데이터)을 추가하여 실제로 데이터가 어떻게 기록되는지 보여준다. 3. **Google Calendar 일정 생성** - 응답이 제출되면, 예약 희망 날짜를 기준으로 기본 1시간짜리 일정을 내 캘린더에 자동 등록한다. - 일정 제목에는 신청자 이름을 포함시킨다. 4. **예약 확인 이메일 발송** - 신청자의 이메일 주소로 확인 메일을 자동 발송한다. - 메일 본문은 HTML 형식으로 작성하고, 예약 정보 요약(이름, 예약일 등)을 포함한다. - 추가로 “캘린더에서 확인하기” 버튼을 넣어, 시각적으로 깔끔한 이메일을 만든다. - (교육 목적이므로 버튼은 실제로 동작하지 않아도 된다.) 5. **전체 자동화 구현** - 위의 모든 과정을 **한 번의 실행으로 세팅**할 수 있도록 구성한다. - 코드는 초보자도 이해할 수 있도록 **충분한 설명 주석**을 포함해야 한다. - 처음 실행 시 필요한 권한 승인 절차도 안내한다. --- ## ✅ 요청 위 요구사항을 모두 충족하는, **실행 즉시 오류 없이 작동하는 Google Apps Script 코드**를 작성해줘. 코드는 단계별 설명과 함께 제공하여, 프로그래밍 경험이 적은 사람도 따라할 수 있도록 해줘.

2. 앱스 스크립트 생성

크롬 URL > 'script.new'
제목 입력하기 : 각자 맘에 드는 것으로
위 페이지의 function 부분 기본 입력되어 있는 code 모두 삭제
위 1번에서 생성된 코드를 붙여넣기.
저장 > 실행
아래와 같이 계속해서 권한 허용
내가 사용할 구글 계정 선택
'고급' 선택
'제목 없는 프로젝트로 이동' 선택
'계속' 선택
'모두 선택'
맨 밑에 '계속' 선택

3. 오류 대응

'실행 로그'에 오류 발생하는 경우, 해당 내용을 스크린 캡쳐 (Shift+Win+S)
ChatGPT에 그대로 붙여넣고 엔터
ChatGPT와 대화식으로 문제를 해결해가기(입코드 디버깅)
ChatGPT에게 모르는 부분을 물어가면서 디버깅
프로그래머/개발자만 이해할 수 있는 내용이라면, '난 개발자가 아니니 쉽게 설명해줘' 명령
ChatGPT가 코드 부분 수정안만을 주면, '코드 전문을 만들어줘' 명령
ChatGPT의 수정안으로 위 2번 과정 반복

4. 테스트 해보기

아래 메시지가 뜨면 에러 없이 잘 실행된 상황

아래는 라이언이 만든 소스 코드

Google Forms에 입력된 응답이 Google Sheets에 저장되고,
응답을 바탕으로 Google Calendar에 1시간짜리 예약 일정이 자동 등록되며,
해당 일정에 고객 이메일로 구글 초대 메일이 자동 발송되고,
동시에 브랜드 스타일의 HTML 확인 메일도 별도로 발송된다.
Form URL : 바로가기
/** * 예약 자동화 시스템 (외부 고객용) * - Google Form (이름, 이메일, 날짜) * - Google Sheet 연결 * - Form 제출 → 캘린더 일정 생성(1시간) + 확인 이메일 발송 * - 이메일 형식은 onFormSubmit에서 정규식으로 검증 * - 샘플 응답 1건 자동 제출 */ /** ====== 사용자 설정 ====== */ const DEFAULT_START_HOUR = 10; // 예약 기본 시작 시각 (24h 기준, 오전 10시) const DEFAULT_DURATION_HOURS = 1; // 예약 기본 지속 시간 (시간) /** ====== 질문 제목 ====== */ const FORM_TITLE = '고객 예약 신청'; const FORM_DESCRIPTION = '아래 항목을 작성해 예약을 신청해 주세요.\n모든 예약은 오전 10시 시작, 1시간 일정으로 등록됩니다.'; const NAME_TITLE = '고객 이름 (예: 홍길동)'; const EMAIL_TITLE = '고객 이메일 (예: test@example.com)'; const DATE_TITLE = '예약 희망 날짜 (YYYY-MM-DD)'; /** ====== 내부 관리용 속성 키 ====== */ const PROP_KEYS = { FORM_ID: 'RESV_FORM_ID', SHEET_ID: 'RESV_SHEET_ID', TRIGGER_ID: 'RESV_FORM_TRIGGER_ID' }; /** * 메인 세팅 함수 (한 번 실행) */ function setupReservationAutomation() { const { form, sheet, isNew } = ensureFormAndSheet_(); ensureFormSubmitTrigger_(form); submitSampleResponse_(form); Logger.log('=== 예약 자동화 세팅 완료 ==='); Logger.log('폼 링크: ' + form.getPublishedUrl()); Logger.log('응답 스프레드시트: https://docs.google.com/spreadsheets/d/' + sheet.getId()); Logger.log('상태: ' + (isNew ? '신규 생성' : '기존 재사용')); } /** * 폼 & 시트 준비 */ function ensureFormAndSheet_() { const props = PropertiesService.getScriptProperties(); let formId = props.getProperty(PROP_KEYS.FORM_ID); let sheetId = props.getProperty(PROP_KEYS.SHEET_ID); let form, sheet, isNew = false; if (formId && sheetId) { form = FormApp.openById(formId); sheet = SpreadsheetApp.openById(sheetId); form.setDestination(FormApp.DestinationType.SPREADSHEET, sheetId); } else { isNew = true; sheet = SpreadsheetApp.create('Reservation_Responses'); form = FormApp.create(FORM_TITLE) .setDescription(FORM_DESCRIPTION) .setAllowResponseEdits(false); // 이름 form.addTextItem().setTitle(NAME_TITLE).setRequired(true); // 이메일 form.addTextItem() .setTitle(EMAIL_TITLE) .setHelpText('예약 확인 메일이 발송됩니다. 정확히 입력해 주세요.') .setRequired(true); // 날짜 form.addDateItem() .setTitle(DATE_TITLE) .setHelpText('YYYY-MM-DD 형식으로 날짜 선택') .setRequired(true); // 예약 시간 선택 (11:00 ~ 20:00) const timeItem = form.addListItem() .setTitle('예약 희망 시간 (1시간 단위)') .setHelpText('11:00 ~ 20:00 중 원하는 시간을 선택해 주세요.') .setRequired(true); // 11:00 ~ 20:00 까지 시간 옵션 추가 const timeOptions = []; for (let hour = 11; hour <= 20; hour++) { timeOptions.push(`${hour}:00`); } timeItem.setChoiceValues(timeOptions); form.setDestination(FormApp.DestinationType.SPREADSHEET, sheet.getId()); props.setProperty(PROP_KEYS.FORM_ID, form.getId()); props.setProperty(PROP_KEYS.SHEET_ID, sheet.getId()); } return { form, sheet, isNew }; } /** * 트리거 보장 */ function ensureFormSubmitTrigger_(form) { const triggers = ScriptApp.getProjectTriggers(); const exists = triggers.some(t => t.getHandlerFunction() === 'onFormSubmit' && t.getTriggerSourceId() === form.getId() ); if (!exists) { ScriptApp.newTrigger('onFormSubmit') .forForm(form) .onFormSubmit() .create(); } } /** * 이메일 형식 검증 함수 */ function isValidEmail_(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } /** * Form 제출 시 처리 */ function onFormSubmit(e) { try { const parsed = parseResponse_(e); if (!parsed) throw new Error('응답 파싱 실패'); const { name, email, date, timeStr } = parsed; if (!isValidEmail_(email)) { Logger.log(`❌ 잘못된 이메일 형식: ${email}`); return; } const { event, start, end } = createCalendarEvent_(name, date, timeStr, email); sendBrandedEmail_(name, email, start, end); Logger.log(`예약 처리 완료: ${name}, ${email}, ${timeStr}`); } catch (err) { Logger.log('[onFormSubmit ERROR] ' + err.message); } } function sendBrandedEmail_(name, email, start, end) { const subject = `[예약 확인] ${name}님, 예약이 완료되었습니다!`; const plain = `${name}님, 안녕하세요!\n\n` + `예약이 정상적으로 접수되었습니다.\n` + `- 예약일시: ${formatDateTime_(start)} ~ ${formatDateTime_(end)}\n\n` + `추가 문의가 있으시면 언제든 연락 주세요.\n\n감사합니다.`; const html = ` <div style="font-family:Arial,sans-serif; max-width:600px; margin:auto; padding:20px; border:1px solid #eee; border-radius:10px;"> <h2 style="color:#1a73e8;">예약 확인 안내</h2> <p><b>${name}</b>님, 예약이 정상적으로 접수되었습니다 🎉</p> <table style="border-collapse:collapse; margin:20px 0; width:100%;"> <tr> <td style="padding:10px; border:1px solid #ddd; background:#f9f9f9; width:30%;">예약자</td> <td style="padding:10px; border:1px solid #ddd;">${name}</td> </tr> <tr> <td style="padding:10px; border:1px solid #ddd; background:#f9f9f9;">예약일시</td> <td style="padding:10px; border:1px solid #ddd;">${formatDateTime_(start)} ~ ${formatDateTime_(end)}</td> </tr> </table> <p style="margin:20px 0;"> <a href="https://calendar.google.com" target="_blank" style="display:inline-block; padding:12px 18px; background:#1a73e8; color:#fff; text-decoration:none; border-radius:6px; font-weight:bold;"> 캘린더에서 확인하기 </a> </p> <p style="color:#666; font-size:12px;">본 메일은 ${new Date().getFullYear()}년 브랜드 예약 시스템에서 발송되었습니다.</p> </div> `; MailApp.sendEmail({ to: email, subject: subject, body: plain, htmlBody: html }); } /** * 응답 파싱 */ function parseResponse_(e) { const itemResponses = e && e.response ? e.response.getItemResponses() : []; let name, email, dateObj, timeStr; itemResponses.forEach(ir => { const title = ir.getItem().getTitle(); const resp = ir.getResponse(); if (title === '고객 이름 (예: 홍길동)') { name = String(resp).trim(); } else if (title === '고객 이메일 (예: test@example.com)') { email = String(resp).trim(); } else if (title.startsWith('예약 희망 날짜')) { dateObj = resp instanceof Date ? resp : new Date(resp); } else if (title.startsWith('예약 희망 시간')) { timeStr = String(resp).trim(); // 예: "13:00" } }); if (!name || !email || !dateObj || !timeStr) return null; return { name, email, date: dateObj, timeStr }; } /** * 캘린더 일정 생성 */ function createCalendarEvent_(name, dateObj, timeStr, email) { // timeStr = "13:00" 형태 const [hour, minute] = timeStr.split(':').map(Number); const start = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), hour, minute, 0); const end = new Date(start.getTime() + 60 * 60 * 1000); // 1시간 고정 const cal = CalendarApp.getDefaultCalendar(); const event = cal.createEvent(`예약 - ${name}`, start, end, { description: `신청자: ${name}\n예약일시: ${formatDateTime_(start)} ~ ${formatDateTime_(end)}` }); // 고객을 게스트로 추가 (구글 초대 메일 자동 발송) event.addGuest(email); return { event, start, end }; } /** * 샘플 응답 제출 */ function submitSampleResponse_(form) { const sampleName = '데모 사용자'; const myEmail = Session.getActiveUser().getEmail() || 'demo@example.com'; const sampleDate = new Date(); const sampleTime = "11:00"; // ✅ 샘플 시간 값 추가 const items = form.getItems(); const itemMap = {}; items.forEach(it => (itemMap[it.getTitle()] = it)); const resp = form.createResponse(); resp.withItemResponse(itemMap[NAME_TITLE].asTextItem().createResponse(sampleName)); resp.withItemResponse(itemMap[EMAIL_TITLE].asTextItem().createResponse(myEmail)); resp.withItemResponse(itemMap[DATE_TITLE].asDateItem().createResponse(sampleDate)); // ✅ 시간 질문 응답 추가 resp.withItemResponse(itemMap['예약 희망 시간 (1시간 단위)'].asListItem().createResponse(sampleTime)); resp.submit(); Logger.log('샘플 응답 제출 완료 (이메일: ' + myEmail + ', 시간: ' + sampleTime + ')'); } /** ====== 유틸 ====== */ function pad2_(n) { return (n < 10 ? '0' : '') + n; } function formatDateTime_(d) { return `${d.getFullYear()}-${pad2_(d.getMonth() + 1)}-${pad2_(d.getDate())} ${pad2_(d.getHours())}:${pad2_(d.getMinutes())}`; }
Contact : azureguy@empal.com / azureguy@cau.ac.kr