구글 설문지로 '견적서 자동 발송' 시스템 구축하기 | 완성 템플릿 포함
구글 설문지 견적서 자동 발송 목차 바로가기
영상 강의
예제파일 다운로드
오빠두엑셀의 강의 예제파일은 여러분을 위해 자유롭게 제공하고 있습니다.
① 구글 설문지 견적서 자동화 서식 테스트
https://bit.ly/google-automate (하루 100건 제한)
https://bit.ly/google-automate (하루 100건 제한)
② 견적서 사본 복사하기
https://docs.google.com/spreadsheets/d/1hXclg0ynU8
pM2a57cLplxMPoTW1a4de4xU9qZjZCa28/copy
https://docs.google.com/spreadsheets/d/1hXclg0ynU8
pM2a57cLplxMPoTW1a4de4xU9qZjZCa28/copy
라이브 강의 전체영상도 함께 확인해보세요!
위캔두 회원이 되시면 매주 오빠두엑셀에서 진행하는 라이브강의 풀영상을 확인하실 수 있습니다.
구글 설문지 응답 확인용 정규표현식
① 이메일 주소 확인 (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자 내외의 섬세하고 꼼꼼한 프롬프트를 영어로 작성해줘.
로고는 심플하고 사람들의 시선을 사로잡을 수 있어야 해.
이미지를 생성에 사용할 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}
고객이 원하는 서비스 종류, 장소, 시간을 작성한 설문지를 제출하였을 때, 감사함을 표시하는 이메일을 보내려고해.
이메일에 사용할 수 있는 세련되고 멋진 html 코드를 작성해줘.
작성 규칙 :
1. 각 개체의 스타일은 스타일 시트 대신 inline-style로 적용합니다.
2. 이메일 상단에는 회사 로고가 약 60px 사이즈로 잘 보이도록 추가합니다.
3. 브랜드 컬러는 "파랑, 남색" 계열로 스타일을 적용합니다.
4. 첫 문장에는 환영 메세지를 작성합니다.
5. 메일에는 아래 항목을 포함해서 작성합니다.
로고 url : {logo_url}
서비스 종류 : {service_type}
행사 구분 : {event_type}
날짜 및 시간 : {service_date} {service_time}
인원 : 여 - {female_count}, 남 - {male_count}

코드 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];
}
구글 설문지로 파일을 첨부하면, 구글 드라이브에 저장됩니다.
DriveApp의 getFilesByName 함수로 파일을 받아온 후, 메일에 첨부하여 보내는 방식으로 코드를 작성해보세요.
Class File | Apps Script | Google for Developers
감사합니다.
더 여러번 보고 질문을 드려야 겠네요
{} 로 받은 데이터도 함수로 사용할 수 있습니다.
다만 ={male_age}*2 와 같이 작성할 경우 문자값으로 받아와 오류가 발생할 수 있으므로, 이전에 변수로 {male_age}를 받은 후 변수*2 와 같이 수식을 작성하는 것을 권장합니다.
감사합니다.
따라서 실습을 하고 있는데요, apps script 실행을 하면 자꾸 같은 에러가 뜹니다. 제 구글 계정이 네이버 메일로 되어 있는데, 그게 이유일까요? ㅠㅠ
네 맞습니다. gmail 계정에서 다시 앱스크립트를 사용해보시거나, 다른 gmail 계정에서 API키를 발급받은 후, 사용하고 계신 naver 계정에 권한을 공유하여 코드를 작성해보세요. :)
GmailApp 을 다른 계정에 연동하는 방법은 아래 링크를 확인해보시길 바랍니다.
How to point to other GMail accounts/folders in Google Apps Script - Stack Overflow
감사합니다
해당 에러에 대해 해결방법 좀 문의드려요. ^^
해당 오류는 시트에 마지막 행이 없을 경우 (시트가 비어있을 경우) 발생합니다.
설문지 응답이 제출되는 시트에 데이터가 올바르게 작성되었는지 한번 확인해보시겠어요? :)
감사합니다.
견적서에 입력한 {client_name} 자리에 설문지 내용이 들어 와야 하는데, 견적서에는 {client_name}라고 값이 그대로 있습니다.
어떻게 해야 {client_name}에 설문지 내용을 불러 올 수 있나요?
아마도 작성하신 코드 혹는 취합한 설문지 시트의 필드명이 다를 경우, {client_name}이 그대로 보일 수 있습니다.
사용하고 계신 시트의 필드명과 코드를 한번 더 검토해보세요. 감사합니다.
해당 오류는 이메일 수신인(recipient)이 누락되어 발생하는 오류입니다.
작성하신 코드를 다시 한번 더 검토해보시길 바랍니다. :)
감사합니다.🙇♂️