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