오빠두엑셀 `2026 무료 챌린지` 오픈! 완주하고 수료증 받아가세요! 5년 연속 IT분야 베스트셀러! 「 진짜쓰는 실무엑셀 」로 2026년 공부 끝내기 엑셀이 막히셨나요? Q&A 게시판에서 바로 해결하세요.
메뉴

구글 설문지로 '견적서 자동 발송' 시스템 구축하기 | 완성 템플릿 포함

모든 직군에 바로 사용 할 수 있는 "100% 무료" 견적서 자동 발송 시스템 만들기! - 구글 설문지와 앱스크립트 만으로 손쉽게 구축하는 방법🔥

# 구글시트

작성자 :
오빠두엑셀
최종 수정일 : 2024. 03. 24. 19:55
URL 복사
메모 남기기 : (31)

구글 설문지로 '견적서 자동 발송' 시스템 구축하기 | 완성 템플릿 포함

구글 설문지 견적서 자동 발송 목차 바로가기
영상 강의


예제파일 다운로드

오빠두엑셀의 강의 예제파일은 여러분을 위해 자유롭게 제공하고 있습니다.

① 구글 설문지 견적서 자동화 서식 테스트
https://bit.ly/google-automate (하루 100건 제한)

.

라이브 강의 전체영상도 함께 확인해보세요!

위캔두 회원이 되시면 매주 오빠두엑셀에서 진행하는 라이브강의 풀영상을 확인하실 수 있습니다.


구글 설문지 응답 확인용 정규표현식

① 이메일 주소 확인 (abc@naver.com 형식)

[a-zA-Z0-9_\.\+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-\.]+

② 휴대폰 번호 확인 (010-1234-1234 형식)

^\d{3}-\d{3,4}-\d{4}$

③ 이름에 띄어쓰기 포함 여부 확인 

\S+

견적서 자동 발송 : 앱 스크립트 마스터 코드

/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */
/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 이 영역을 수정하세요. ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */
 
const sourceSpreadsheetId = '구글시트ID';
const destinationFolderId = '폴더ID';
const sh_name_inv = '견적서'; // 업데이트 할 서식의 시트명
const sh_name_form = '설문지'; // 설문지와 연동된 시트명
const printArea = 'A1:I34'; // PDF로 출력할 범위
 
const logo_url = '로고URL' // 빙 이미지 생성기에서 만든 이미지 URL을 작성합니다.;
 
const mailtitle = '[오빠두엑셀] {client_name} 님의 강의요청서입니다.';
const htmlbody = `{client_name}님 감사합니다.`;
 
const fileNameFormat = "OPD{rowID}-{client_name}-{datetime}";
const sendAsPDF = 1;
 
// 견적서 서식과 동일하게 {필드}로 메일 본문 내용을 수정할 수 있습니다.
// {rowID}는 설문지 시트에서 고유 ID로 사용할 수 있는 행 번호를 5자리 숫자로 반환합니다.
// {datetime}은 현재 시간을 yymmddhhmmss 형식으로 반환합니다.
// PDF를 첨부하지 않고, 구글 시트 링크를 보낼 경우 sendAsPDF를 0으로 사용합니다.
 
/* ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 이 영역을 수정하세요. ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ */
/* ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ */
 
// 설문지 데이터를 기반으로 문서를 생성하는 함수입니다.
function sendEmailfromForm() {
 
// 소스 스프레드시트를 엽니다.
var sourceSpreadsheet = SpreadsheetApp.openById(sourceSpreadsheetId);
 
// '설문지' 시트에서 새롭게 추가된 항목을 배열로 가져옵니다.
var sh = sourceSpreadsheet.getSheetByName(sh_name_form);
var last_row = sh.getLastRow();
var rng = sh.getDataRange();
var values = rng.getValues()[last_row - 1];
var header = rng.getValues()[0];
 
// 파일이름에 사용할 현재 시간을 파싱합니다.
var formattedTime = Utilities.formatDate(new Date(), 'Asia/Seoul', 'yyMMddHHmmss');
 
// 가져온 설문지 데이터를 파싱합니다.
var formdata = parseFormData(values, header);
 
// 파싱된 데이터를 콘솔에 출력합니다.
console.log(formdata)
 
// '견적서' 템플릿을 복사합니다.
var sh_inv = sourceSpreadsheet.getSheetByName(sh_name_inv);
var sh_copy = sh_inv.copyTo(sourceSpreadsheet);
 
// 복사된 시트의 데이터 범위를 참조합니다.
var new_rng = sh_copy.getDataRange();
var new_values = new_rng.getValues();
 
// 복사된 시트의 모든 셀을 돌아가벼 반복문을 실행합니다.
for (var i = 0; i < new_values.length; i++) {
for (var j = 0; j < new_values[i].length; j++) {
var cell = new_values[i][j];
// 셀이 문자열이고 '{'와 '}'로 시작하고 끝나는지 확인합니다.
if (typeof cell === 'string' && cell.startsWith('{') && cell.endsWith('}')) {
var key = cell.substring(1, cell.length - 1);
// 셀에 'rowID'라는 키가 있으면 마지막 행 번호로 값을 설정합니다.
if (key == 'rowID') {
sh_copy.getRange(i + 1,j + 1).setValue(last_row);
} else {
// 그렇지 않으면 해당 키에 해당하는 데이터로 값을 설정합니다.
sh_copy.getRange(i + 1,j + 1).setValue(formdata[key]);
}
}
}
}
 
// 스프레드시트에 대기 중인 변경사항을 적용합니다.
SpreadsheetApp.flush();
 
// 완성된 견적서 시트의 함수를 값으로 변경합니다.
rng = sh_copy.getDataRange();
values =rng.getValues();
rng.setValues(values);
 
// 새로운 스프레드시트를 생성합니다.
var newSpreadsheet = SpreadsheetApp.create('Sheet');
var newSpreadsheetId = newSpreadsheet.getId();
var sh_copied = sh_copy.copyTo(newSpreadsheet);
 
// 완성된 견적서를 새 스프레드시트로 옮깁니다.
sh_copied.setName(sh_name_inv);
sourceSpreadsheet.deleteSheet(sh_copy);
 
// 새 스프레드시트의 기본시트를 제거하고, 견적서 시트만 남깁니다.
var defaultSheet = newSpreadsheet.getSheets()[0];
newSpreadsheet.deleteSheet(defaultSheet);
 
var emailbody = htmlbody;
var emailtitle = mailtitle;
var fileName = fileNameFormat;
// 이메일 본문 중 "{}"로 작성된 값을 고객이 제출한 설문지 데이터로 변경합니다.
for (var key in formdata) {
if (formdata.hasOwnProperty(key)) {
 
// 정규표현식을 작성합니다.
var regex = new RegExp('{' + key + '}', 'g');
 
// 필드명에 date, time이 있을 경우 표시형식을 날짜, 시간 형식으로 변경합니다.
if (key.toLowerCase().includes("date")) {
var replacevalue = Utilities.formatDate(formdata[key], 'Asia/Seoul', 'yyyy-MM-dd');
} else if (key.toLowerCase().includes("time")) {
var replacevalue = Utilities.formatDate(formdata[key], 'Asia/Seoul', 'a hh:mm:ss');
} else {
var replacevalue = formdata[key];
}
 
// "{키}" 를 설문지 값으로 변경합니다.
emailbody = emailbody.replace(regex, replacevalue);
emailtitle = emailtitle.replace(regex, replacevalue);
fileName = fileName.replace(regex,replacevalue);
}
}
// {logo_url}이 있을 경우, 실제 로고 이미지 주소로 변경합니다.
emailbody = emailbody.replace("{logo_url}",logo_url);
fileName = fileName.replace("{datetime}",formattedTime);
fileName = fileName.replace("{rowID}",last_row.toString().padStart(5,'0'));
 
// 새 스프레드시트를 지정한 폴더에 저장합니다.
var file = DriveApp.getFileById(newSpreadsheetId);
file.setName(fileName);
var folder = DriveApp.getFolderById(destinationFolderId);
file.moveTo(folder);
 
if (sendAsPDF == 1) {
// 고객에게 PDF 첨부파일과 함께 이메일을 발송합니다.
var fileId = SaveAsPDF(newSpreadsheetId, sh_name_inv, printArea, destinationFolderId, fileName+".pdf");
sendEmailWithAttachment(formdata['client_email'], emailtitle, emailbody, fileId);
} else {
var driveShFile = DriveApp.getFileById(newSpreadsheetId);
driveShFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
emailbody = '<div style="text-align: center; background: #3f37bb; color: #fff; max-width: 600px; border-radius: 5px; margin: auto; margin-top: 10px; margin-bottom: 10px; line-height: 1.5rem; padding: 10px;">발행된 문서는 아래 링크를 클릭해서 확인하세요.<div style="color: #fff34e; font-weight: bold; font-size: 0.8rem;">'+newSpreadsheet.getUrl()+'</div></div>' + emailbody;
sendEmailWithAttachment(formdata['client_email'], emailtitle, emailbody);
}
}
 
// 헤더와 값을 딕셔너리로 변환하는 함수입니다.
function parseFormData(values, header) {
 
// 비어있는 배열을 생성합니다.
var result = {};
 
// 항목을 하나씩 돌아가며 헤더와 값을 하나의 배열로 합칩니다.
for (var i = 0; i < values.length; i++) {
var key = header[i];
var value = values[i];
 
// 키에 count 라는 단어가 포함된 경우, 값에서 숫자만 남기고 '명'을 지웁니다.
if (key.toLowerCase().includes("count")) {
value = value.toString().replace("명", "");
}
result[key] = value;
}
 
return result;
}
 
// 시트를 PDF로 저장하는 함수입니다.
function SaveAsPDF(sheetId, sheetName, printRange, folderId, pdfFileName) {
// PDF로 저장할 시트를 불러옵니다.
var folder = DriveApp.getFolderById(folderId);
var spreadsheet = SpreadsheetApp.openById(sheetId);
var sheet = spreadsheet.getSheetByName(sheetName);
 
// 시트를 PDF로 저장하기 위한 URL을 생성합니다.
var url = spreadsheet.getUrl().replace(/edit$/, '')
+ 'export?exportFormat=pdf&format=pdf'
// Specify the range and sheet name
+ '&gid=' + sheet.getSheetId()
+ '&range=' + printRange
// Add other parameters for PDF formatting as needed
+ '&size=A4'
+ '&portrait=true'
+ '&scale=4'
+ '&sheetnames=false&printtitle=false&pagenumbers=false'
+ '&gridlines=false'
+ '&fzr=false';
 
// 보안 토큰을 헤더에 추가합니다.
var token = ScriptApp.getOAuthToken();
var response = UrlFetchApp.fetch(url, {
headers: {
'Authorization': 'Bearer ' + token
}
});
 
// 완성된 PDF를 구글 드라이브 폴더에 저장합니다.
var pdfBlob = response.getBlob().setName(pdfFileName);
var pdfFile = folder.createFile(pdfBlob);
 
return pdfFile.getId();
}
 
// PDF 첨부파일과 함께 이메일을 발송하는 함수입니다.
function sendEmailWithAttachment(emailaddress, title, body, fileId) {
var recipient = emailaddress;
var subject = title;
var htmlBody = body;
 
if (fileId == null) {
GmailApp.sendEmail(recipient, subject, '', {
htmlBody: htmlBody
});
} else {
var file = DriveApp.getFileById(fileId);
GmailApp.sendEmail(recipient, subject, '', {
htmlBody: htmlBody,
attachments: [file.getAs(MimeType.PDF)]
});
}
 
}

이번 강의에서 사용한 ChatGPT 프롬프트

① ChatGPT에 로고 생성 요청하기

DallE-3 모델을 써서 헤어&메이크업 뷰티 브랜드를 위한 로고를 만들려고해.
로고는 심플하고 사람들의 시선을 사로잡을 수 있어야 해.
이미지를 생성에 사용할 200자 내외의 섬세하고 꼼꼼한 프롬프트를 영어로 작성해줘.

② ChatGPT로 이메일 HTML 본문 만들기

현재 고객에게 헤어 스타일링과 메이크업을 제공하는 사업을 운영하고 있어. 회사 이름은 "갓빠두 뷰티 살롱" 이야.
고객이 원하는 서비스 종류, 장소, 시간을 작성한 설문지를 제출하였을 때, 감사함을 표시하는 이메일을 보내려고해.
이메일에 사용할 수 있는 세련되고 멋진 html 코드를 작성해줘.
작성 규칙 :
1. 각 개체의 스타일은 스타일 시트 대신 inline-style로 적용합니다.
2. 이메일 상단에는 회사 로고가 약 60px 사이즈로 잘 보이도록 추가합니다.
3. 브랜드 컬러는 "파랑, 남색" 계열로 스타일을 적용합니다.
4. 첫 문장에는 환영 메세지를 작성합니다.
5. 메일에는 아래 항목을 포함해서 작성합니다.
로고 url : {logo_url}
서비스 종류 : {service_type}
행사 구분 : {event_type}
날짜 및 시간 : {service_date} {service_time}
인원 : 여 - {female_count}, 남 - {male_count}
댓글 31
4.9 (16개 평가)
정재연 실장
정재연 실장 2024.01.21 19:21
날짜또는 시간의 포맷이 자꾸 에러가 뜹니다. ㅠㅠ 5시간째... ㅠㅠ해메고 있습니다. 도와주세요.. 한번 성공했었는데.~~ 아쉬워서 계속 하고 있어요.
20240121_192041
오빠두엑셀
오빠두엑셀 작성자 2024.01.28 14:17
안녕하세요.
코드 109번째 줄, 아마도 아래 부분일 것 같은데요
필드명에 date나 time이 있을경우, 강제로 시간/날짜 서식으로 변경하도록 코드가 작성되어 있습니다. 코드에서 아래 영역을 삭제 후 실행하시거나, 필드명에서 date 또는 time을 지운 후 다시 실행해보세요.
바로 해결될겁니다.^^ 감사합니다.
if (key.toLowerCase().includes("date")) {
var replacevalue = Utilities.formatDate(formdata[key], 'Asia/Seoul', 'yyyy-MM-dd');
} else if (key.toLowerCase().includes("time")) {
var replacevalue = Utilities.formatDate(formdata[key], 'Asia/Seoul', 'a hh:mm:ss');
} else {
var replacevalue = formdata[key];
}
Lilyy
Lilyy 2024.02.05 18:59
제가 너무 찾던 강의인데 혹시 저는 구글 설문지 폼에 파일업로드 (PDF,JPG,PNG) 필드를 추가할건데 설문지 작성이 완료된 뒤 견적서 자동화 메일을 보낼 때 업로드된 파일 (PDF,JPG,PNG)까지 같이 메일이 전송이 될 수 있도록 하려면 어떻게 해야하는지 알 수 있을까요?
오빠두엑셀
오빠두엑셀 작성자 2024.02.10 05:51
안녕하세요. 오빠두엑셀입니다.
구글 설문지로 파일을 첨부하면, 구글 드라이브에 저장됩니다.
DriveApp의 getFilesByName 함수로 파일을 받아온 후, 메일에 첨부하여 보내는 방식으로 코드를 작성해보세요.
Class File | Apps Script | Google for Developers
감사합니다.
RayLee
RayLee 2024.02.14 15:23
아 한참을 봐도 아직 이해력이 부족한거 같습니다.
더 여러번 보고 질문을 드려야 겠네요
o****
o**** 2024.02.14 17:44
한가지 궁금한게 있는데 {} 내용으로 받은 데이터는 함수에 사용이 불가능할까요? ex) ={male_age}*2 이런 데이터나 함수 포함하여 메일 견적서 상에 적용되게 만들 수 있는 방법은 없나요?
오빠두엑셀
오빠두엑셀 작성자 2024.02.20 20:38
안녕하세요.
{} 로 받은 데이터도 함수로 사용할 수 있습니다.
다만 ={male_age}*2 와 같이 작성할 경우 문자값으로 받아와 오류가 발생할 수 있으므로, 이전에 변수로 {male_age}를 받은 후 변수*2 와 같이 수식을 작성하는 것을 권장합니다.
감사합니다.
shock****
shock**** 2024.02.17 15:27
안녕하세요, 강의 감사 드립니다.
따라서 실습을 하고 있는데요, apps script 실행을 하면 자꾸 같은 에러가 뜹니다. 제 구글 계정이 네이버 메일로 되어 있는데, 그게 이유일까요? ㅠㅠ
주석-2024-02-17-152622
오빠두엑셀
오빠두엑셀 작성자 2024.02.21 05:43
안녕하세요.
네 맞습니다. gmail 계정에서 다시 앱스크립트를 사용해보시거나, 다른 gmail 계정에서 API키를 발급받은 후, 사용하고 계신 naver 계정에 권한을 공유하여 코드를 작성해보세요. :)
GmailApp 을 다른 계정에 연동하는 방법은 아래 링크를 확인해보시길 바랍니다.
How to point to other GMail accounts/folders in Google Apps Script - Stack Overflow
감사합니다
이승수
이승수 2024.02.23 20:38
안녕하세요.
해당 에러에 대해 해결방법 좀 문의드려요. ^^
20240223_203744
오빠두엑셀
오빠두엑셀 작성자 2024.02.26 17:55
안녕하세요.
해당 오류는 시트에 마지막 행이 없을 경우 (시트가 비어있을 경우) 발생합니다.
설문지 응답이 제출되는 시트에 데이터가 올바르게 작성되었는지 한번 확인해보시겠어요? :)
감사합니다.
레스
레스 2024.03.14 15:45
질문 있습니다.

견적서에 입력한 {client_name} 자리에 설문지 내용이 들어 와야 하는데, 견적서에는 {client_name}라고 값이 그대로 있습니다.

어떻게 해야 {client_name}에 설문지 내용을 불러 올 수 있나요?
오빠두엑셀
오빠두엑셀 작성자 2024.03.20 00:06
안녕하세요.
아마도 작성하신 코드 혹는 취합한 설문지 시트의 필드명이 다를 경우, {client_name}이 그대로 보일 수 있습니다.
사용하고 계신 시트의 필드명과 코드를 한번 더 검토해보세요. 감사합니다.
zs-
zs- 2024.04.24 11:37
감사합니다
전정민
전정민 2024.06.12 12:31
아래 댓글중 shock****님의 같은 코드 에서 오류가 발생했습니다만. 다른점은 제 구글 계정은 네이버가 아닙니다. 현재 실행 버튼을 누르면 드라이브에는 파일들이 pdf형식과 구글시트로 저장은 되나, 이메일은 빈칸으로 저장이 됩니다.
KakaoTalk_20240612_123009840
KakaoTalk_20240612_120837907
오빠두엑셀
오빠두엑셀 작성자 2024.06.13 04:14
안녕하세요. 오빠두엑셀입니다.
해당 오류는 이메일 수신인(recipient)이 누락되어 발생하는 오류입니다.
작성하신 코드를 다시 한번 더 검토해보시길 바랍니다. :)
감사합니다.🙇‍♂️
강민준🤗
강민준🤗 2024.08.12 10:25
좋은 강의 정말 감사합니다🙇‍♂️