# 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](mailto: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' 

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/161722_fsCuNaddrnUtHy5uWC?q=80&s=1280x180&t=outside&f=webp)

- 제목 입력하기 : 각자 맘에 드는 것으로 

- 위 페이지의 function 부분 기본 입력되어 있는 code 모두 삭제

- 위 1번에서 생성된 코드를 붙여넣기.

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/162322_NRwfIUBdwTu2kfN84K?q=80&s=1280x180&t=outside&f=webp)

- 저장 > 실행

- 아래와 같이 계속해서 권한 허용

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/162230_4sKepRrG6zftqXgtSH?q=80&s=1280x180&t=outside&f=webp)

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/162546_HOmTauwYDsxhzkiqlY?q=80&s=1280x180&t=outside&f=webp)

- 내가 사용할 구글 계정 선택

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/162620_ex8JxuP4SnIYbqIXm3?q=80&s=1280x180&t=outside&f=webp)

- '고급' 선택

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/162731_9PvpjxYo1tH44HiFTV?q=80&s=1280x180&t=outside&f=webp)

- '제목 없는 프로젝트로 이동' 선택

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/162840_9m3cni0iNcttvK4BcW?q=80&s=1280x180&t=outside&f=webp)

- '계속' 선택

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/162939_b0l5ScN4hUn4iiQrPc?q=80&s=1280x180&t=outside&f=webp)

- '모두 선택'

- 맨 밑에 '계속' 선택

# 3. 오류 대응

- '실행 로그'에 오류 발생하는 경우, 해당 내용을 스크린 캡쳐 (Shift+Win+S)

![Image](https://upload.cafenono.com/image/slashpageHome/20250920/163057_yNWwcv1TqIvv3vqp4I?q=80&s=1280x180&t=outside&f=webp)

- ChatGPT에 그대로 붙여넣고 엔터

- ChatGPT와 대화식으로 문제를 해결해가기(입코드 디버깅)

    - ChatGPT에게 모르는 부분을 물어가면서 디버깅

    - 프로그래머/개발자만 이해할 수 있는 내용이라면, '난 개발자가 아니니 쉽게 설명해줘' 명령

    - ChatGPT가 코드 부분 수정안만을 주면, '코드 전문을 만들어줘' 명령

- ChatGPT의 수정안으로 위 2번 과정 반복

# 4. 테스트 해보기

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

![Image](https://upload.cafenono.com/image/slashpageHome/20250926/160124_X1COVGQQKeZjIb5CN1?q=80&s=1280x180&t=outside&f=webp)

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

- Google Forms에 입력된 응답이 Google Sheets에 저장되고,

- 응답을 바탕으로 Google Calendar에 1시간짜리 예약 일정이 자동 등록되며,

- 해당 일정에 고객 이메일로 구글 초대 메일이 자동 발송되고,

- 동시에 브랜드 스타일의 HTML 확인 메일도 별도로 발송된다.

    - Form URL : [바로가기](https://docs.google.com/forms/d/e/1FAIpQLScvU3xf48noSSyS5QLKQMO3Kdvv74Fre6-_qFHJRWCBTNxanQ/viewform)

```
/**
 * 예약 자동화 시스템 (외부 고객용)
 * - 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())}`;
}

```

For the site tree, see the [root Markdown](https://slashpage.com/lion.md).
