시작하기 전에
- PC 브라우저와 개인 Google 계정이 필요합니다. 복사할 Drive, 읽을 캘린더, 받을 이메일은 같은 계정을 사용합니다.
- 일정이 없어도 시작할 수 있습니다. 기본값은 한국 시간 오전 7~8시 사이, 주말 포함입니다. AI API 키는 필요 없습니다.
설정 시트를 내 계정으로 복사하기
아래 버튼으로 Google 복사 화면을 엽니다. 로그인할 때는 일정을 읽고 메일을 받을 계정을 사용하세요. 사본 만들기를 누르면 내 Drive에 설정 시트가 생깁니다.
이 파일에는 오늘 일정을 읽고 메일을 보내는 코드가 이미 연결되어 있습니다. 코드 편집기를 열거나 내용을 붙여넣지 않아도 됩니다. 공개된 원본이 아닌 내가 복사한 파일에서 다음 작업을 진행하세요.
- 확인할 결과
- 내가 수정할 수 있는 설정 시트가 열립니다. 받을 시각·받을 요일·메일에 함께 넣을 체크리스트가 보입니다.
복사만으로는 메일을 보내거나 예약하지 않습니다.
시작하기를 누르고 Google 권한 승인하기
복사한 시트 위쪽의 일정 메일 메뉴에서 시작하기를 누릅니다. 이 작업은 본인에게 테스트 메일 한 통을 보내고 매일 아침 발송을 예약합니다. 처음에는 설정을 바꾸지 않고 그대로 시작해도 됩니다.
처음 실행하면 승인 필요 안내에 따라 계정을 선택합니다. Google에서 확인하지 않은 앱 화면이 표시되면 아래 캡처와 권한 안내를 확인하고 승인을 진행하세요.
계속하기 전에 화면의 개발자 계정이 본인인지, 방금 복사한 파일에서 시작한 것인지 확인하세요. 요청하는 범위는 현재 시트 수정, 캘린더 읽기, 메일 보내기, 계정 이메일 확인, 자동 실행 예약 관리입니다. 받은편지함을 읽거나 캘린더 일정을 수정하는 권한은 요청하지 않습니다. 화면에 다른 계정이나 예상하지 않은 권한이 나오면 진행하지 말고 해당 문구부터 확인하세요.
권한을 승인한 뒤 자동 발송 중 상태가 보이는지 확인하세요. 승인만 끝나고 결과가 없다면 시트로 돌아와 일정 메일 → 시작하기를 다시 누릅니다. 같은 파일에서 다시 눌러도 이미 요청한 테스트 메일과 예약을 계속 추가하지 않도록 처리합니다.

- 확인할 결과
- 시트 위쪽 “현재 상태”에 “자동 발송 중”이 표시됩니다.
메뉴가 안 보이면 PC에서 복사본을 새로고침하세요. 승인 화면이 다르거나 차단되면 아래 AI 도움에 해당 문구를 전달하세요.
테스트 메일이 도착하면 준비 끝
같은 Google 계정의 받은편지함에서 제목에 테스트가 붙은 오늘 일정 메일을 확인합니다. 오늘 일정과 체크리스트가 담겨 있으면 기본 설정은 끝입니다. AI에게 완료를 보고하거나 추가로 예약을 만들 필요는 없습니다.
오늘 일정이 없으면 오늘 등록된 일정은 없습니다.가 나오는 것이 정상입니다. 이후 본인 기본 캘린더에 평소처럼 일정을 등록하면 그날 메일에 반영됩니다. 공유·구독·공휴일 캘린더는 기본 범위에 포함하지 않습니다.
다음 발송 시간에 테스트 표시 없는 메일이 오면 정기 발송까지 확인된 것입니다. 기본 시간은 한국 시간 오전 7~8시 사이이며 정확한 정각 발송은 아닙니다. 컴퓨터나 시트를 계속 켜 둘 필요는 없습니다.
- 확인할 결과
- 지금: 테스트 메일에서 일정과 체크리스트를 확인합니다.
- 다음 발송 시간: 테스트 표시 없는 일반 메일이 도착합니다.
메일이 보이지 않으면 받은 계정과 스팸함부터 확인하세요. 테스트 메일 발송 요청과 실제 수신은 다릅니다.
시간과 문구는 시트에서 바꾸세요
나중에 바꾸고 싶을 때 설정 시트를 다시 엽니다. 받을 시각은 받고 싶은 시간의 숫자(예: 오전 8시는 8), 받을 요일은 매일 또는 평일만으로 설정합니다. 메일에 함께 넣을 체크리스트에는 메일마다 보고 싶은 문장을 한 줄씩 적습니다.
수정한 뒤 위쪽 일정 메일 → 설정 적용을 누릅니다. 코드 수정 없이 새 설정을 반영하며 별도 테스트 메일을 보내지는 않습니다. 예약 시각은 선택한 시간부터 한 시간 사이입니다.
그만 받고 싶으면 일정 메일 → 중지하기를 누릅니다. 이 파일의 정기 발송을 멈추며 기존 일정과 파일은 그대로 남습니다. 나중에 다시 받으려면 시작하기를 누릅니다. 상태 확인으로 현재 설정과 예약 상태를 볼 수도 있습니다.
코드를 직접 살펴보려면
전체 실행 코드 · Code.gs
/**
* @OnlyCurrentDoc
* 오늘 일정 메일 — 복사한 시트에서 일정 메일 → 시작하기를 누르세요.
* 기본 캘린더를 읽고 실행 계정 본인에게만 보냅니다. 일정은 수정하지 않습니다.
* 시간 예약은 저장된 설정만 읽으며 시트나 AI 채팅을 열어 둘 필요가 없습니다.
*/
var DAILY_MAIL = {
sendHour: 7, // 한국 시간 오전 7~8시 사이. 정확한 분 단위 예약은 아닙니다.
skipWeekends: false,
checklist: [
'오늘 먼저 끝낼 일 한 가지 정하기',
'답장해야 할 문의 확인하기',
'내일 일정 미리 보기'
]
};
function setupDailyMail() {
return withDailyMailLock_(setupDailyMail_);
}
function setupDailyMail_() {
var properties = PropertiesService.getScriptProperties();
requireDailyMailPermissions_(Boolean(properties.getProperty('DAILY_MAIL_SOURCE_SHEET_ID')));
var testSent = Boolean(properties.getProperty('DAILY_MAIL_SETUP_TEST_SENT_TO'));
var schedulingStarted = false;
try {
var mail = buildDailyMail_(new Date());
bindDailyMailOwner_(mail.to, properties);
testSent = properties.getProperty('DAILY_MAIL_SETUP_TEST_SENT_TO') === mail.to.toLowerCase();
if (!testSent) {
sendDailyMailMessage_(mail, true);
properties.setProperty('DAILY_MAIL_SETUP_TEST_SENT_TO', mail.to.toLowerCase());
testSent = true;
}
schedulingStarted = true;
ensureDailyTrigger_(properties);
if (properties.getProperty('DAILY_MAIL_SOURCE_SHEET_ID')) properties.setProperty('DAILY_MAIL_ENABLED', 'true');
console.log('설치 완료: 본인에게 테스트 메일 발송을 요청한 기록과 매일 한국 시간 ' + DAILY_MAIL.sendHour + '~' +
(DAILY_MAIL.sendHour + 1) + '시 사이 예약 1개를 확인했습니다. 다시 실행해도 설정 테스트 메일을 재발송하지 않습니다.');
console.log('실제 수신은 받은편지함 또는 스팸함에서 확인하세요. 다음 예약 실행과 실제 메일 도착은 아직 확인하지 않았습니다.');
} catch (error) {
var testStatus = testSent ? '테스트 메일 발송 요청은 완료되었습니다. 재실행해도 이 테스트를 다시 보내지 않습니다. ' :
'테스트 메일 발송 요청 완료 기록이 없습니다. ';
var triggerStatus = schedulingStarted ? '자동 예약 설정은 완료를 확인하지 못했습니다. ' : '자동 예약 설정은 아직 진행하지 않았습니다. ';
throw new Error(testStatus + triggerStatus + '원인: ' + error.message);
}
}
function previewDailyMail() {
var mail = buildDailyMail_(new Date());
console.log('[미리보기 · 메일을 보내지 않았습니다]\n받는 사람: ' + mail.to + '\n제목: ' + mail.subject + '\n\n' + mail.body);
if (mail.skipToday) console.log('주말 건너뛰기가 켜져 있어 오늘 예약 발송은 생략됩니다. 테스트 발송은 가능합니다.');
return mail;
}
function sendTestMail() {
return withDailyMailLock_(function () {
sendDailyMailMessage_(buildDailyMail_(new Date()), true);
console.log('테스트 메일 한 통의 발송을 요청했습니다. 실제 수신은 받은편지함 또는 스팸함에서 확인하세요. 이 진단 함수는 누를 때마다 한 통씩 요청하며 예약은 만들지 않습니다.');
});
}
function sendDailyMail() {
return withDailyMailLock_(function () {
var properties = PropertiesService.getScriptProperties();
if (properties.getProperty('DAILY_MAIL_SOURCE_SHEET_ID') && properties.getProperty('DAILY_MAIL_ENABLED') !== 'true') {
console.log('자동 발송이 중지되어 있습니다. 시트에서 시작하기를 누르기 전에는 보내지 않습니다.');
return;
}
var mail = buildDailyMail_(new Date());
if (mail.skipToday) { console.log('주말 건너뛰기: 오늘은 발송하지 않습니다.'); return; }
var properties = PropertiesService.getScriptProperties();
if (properties.getProperty('DAILY_MAIL_SENT_DATE') === mail.dateKey) {
console.log('오늘 예약 메일 발송을 이미 요청했습니다. 중복 발송하지 않습니다.');
return;
}
sendDailyMailMessage_(mail, false);
properties.setProperty('DAILY_MAIL_SENT_DATE', mail.dateKey);
console.log('오늘 일정 메일 발송을 요청했습니다.');
});
}
function installDailyTrigger() {
return withDailyMailLock_(function () {
var mail = buildDailyMail_(new Date());
var properties = PropertiesService.getScriptProperties();
bindDailyMailOwner_(mail.to, properties);
ensureDailyTrigger_(properties);
console.log('매일 한국 시간 ' + DAILY_MAIL.sendHour + '~' + (DAILY_MAIL.sendHour + 1) + '시 사이 예약 1개를 확인했습니다. 이 함수는 메일을 보내지 않습니다.');
});
}
function stopDailyMail() {
return withDailyMailLock_(stopDailyMail_);
}
function stopDailyMail_() {
var properties = PropertiesService.getScriptProperties();
if (properties.getProperty('DAILY_MAIL_SOURCE_SHEET_ID')) properties.setProperty('DAILY_MAIL_ENABLED', 'false');
var triggers = dailyMailTriggers_();
triggers.forEach(function (trigger) { ScriptApp.deleteTrigger(trigger); });
if (dailyMailTriggers_().length) throw new Error('아직 남은 예약이 있습니다. stopDailyMail을 다시 실행하세요.');
console.log('이 프로젝트에서 내 계정이 만든 오늘 일정 메일 예약 ' + triggers.length + '개를 중지했습니다. 코드와 캘린더 일정은 남아 있습니다.');
}
function withDailyMailLock_(action) {
var lock = LockService.getScriptLock();
if (!lock.tryLock(10000)) throw new Error('다른 설정 또는 발송이 진행 중입니다. 잠시 후 실행 기록을 확인하고 다시 실행하세요.');
try { return action(); } finally { lock.releaseLock(); }
}
function bindDailyMailOwner_(address, properties) {
// 다른 계정이 공유받은 프로젝트의 설치 기록을 덮어쓰지 않습니다.
properties.setProperty('DAILY_MAIL_OWNER', address.toLowerCase());
}
function ensureDailyTrigger_(properties) {
requireDailyMailPermissions_(Boolean(properties.getProperty('DAILY_MAIL_SOURCE_SHEET_ID')));
var old = dailyMailTriggers_();
var savedId = properties.getProperty('DAILY_MAIL_TRIGGER_ID');
var savedHour = properties.getProperty('DAILY_MAIL_TRIGGER_HOUR');
var current = old.filter(function (trigger) { return trigger.getUniqueId() === savedId; })[0];
if (!current || savedHour !== String(DAILY_MAIL.sendHour)) {
// 새 예약을 만들기 전에는 기존 예약을 지우지 않습니다.
current = ScriptApp.newTrigger('sendDailyMail').timeBased().atHour(DAILY_MAIL.sendHour)
.everyDays(1).inTimezone('Asia/Seoul').create();
properties.setProperty('DAILY_MAIL_TRIGGER_ID', current.getUniqueId());
properties.setProperty('DAILY_MAIL_TRIGGER_HOUR', String(DAILY_MAIL.sendHour));
}
old.forEach(function (trigger) {
if (trigger.getUniqueId() !== current.getUniqueId()) ScriptApp.deleteTrigger(trigger);
});
var remaining = dailyMailTriggers_();
if (remaining.length !== 1 || remaining[0].getUniqueId() !== current.getUniqueId()) {
throw new Error('예약이 정확히 하나인지 확인하지 못했습니다. setupDailyMail을 다시 실행하세요.');
}
}
function dailyMailTriggers_() {
return ScriptApp.getProjectTriggers().filter(function (trigger) {
return trigger.getHandlerFunction() === 'sendDailyMail' && trigger.getEventType() === ScriptApp.EventType.CLOCK;
});
}
function validateDailyMail_() {
var address = String(Session.getEffectiveUser().getEmail() || '').trim();
if (!/^[^\s@,;]+@[^\s@,;]+\.[^\s@,;]+$/.test(address)) {
throw new Error('실행 계정의 이메일을 자동으로 확인하지 못했습니다. 개인 Google 계정의 새 Apps Script 프로젝트에서 실행하세요. 회사·학교 계정은 관리자 정책을 확인하세요.');
}
var owner = PropertiesService.getScriptProperties().getProperty('DAILY_MAIL_OWNER');
if (owner && owner !== address.toLowerCase()) {
throw new Error('이 프로젝트는 다른 계정으로 설정되어 있습니다. 본인 계정에서 새 프로젝트를 만들어 실행하세요.');
}
if (!Number.isInteger(DAILY_MAIL.sendHour) || DAILY_MAIL.sendHour < 0 || DAILY_MAIL.sendHour > 23) {
throw new Error('sendHour에는 0부터 23까지의 정수 한 개를 적으세요.');
}
if (typeof DAILY_MAIL.skipWeekends !== 'boolean') throw new Error('skipWeekends는 true 또는 false로 적으세요.');
if (!Array.isArray(DAILY_MAIL.checklist) || DAILY_MAIL.checklist.length < 1 || DAILY_MAIL.checklist.length > 20 || DAILY_MAIL.checklist.some(function (text) {
return typeof text !== 'string' || !text.trim() || text.length > 200;
})) throw new Error('checklist에는 200자 이내의 문장을 1~20개 적으세요.');
return address;
}
function buildDailyMail_(now) {
loadDailyMailSettings_();
var address = validateDailyMail_();
if (!(now instanceof Date) || isNaN(now.getTime())) throw new Error('현재 날짜를 확인할 수 없습니다.');
var calendar = CalendarApp.getDefaultCalendar();
if (!calendar) throw new Error('기본 캘린더를 찾지 못했습니다. Google 캘린더를 같은 계정으로 열어 확인하세요.');
var timeZone = 'Asia/Seoul';
var dateKey = Utilities.formatDate(now, timeZone, 'yyyy-MM-dd');
var day = Number(Utilities.formatDate(now, timeZone, 'u')) % 7;
var today = dateKey + ' (' + ['일', '월', '화', '수', '목', '금', '토'][day] + ')';
// 명시적인 한국 자정 범위이므로 프로젝트·캘린더 시간대를 변경할 필요가 없습니다.
var start = new Date(dateKey + 'T00:00:00+09:00');
var end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
var events = calendar.getEvents(start, end).slice().sort(function (a, b) {
if (a.isAllDayEvent() !== b.isAllDayEvent()) return a.isAllDayEvent() ? -1 : 1;
return a.getStartTime().getTime() - b.getStartTime().getTime();
});
var schedule = events.map(function (event) {
if (event.isAllDayEvent()) return '종일 · ' + event.getTitle();
var eventStart = event.getStartTime();
var startDay = Utilities.formatDate(eventStart, timeZone, 'yyyy-MM-dd');
var time = Utilities.formatDate(eventStart, timeZone, startDay === dateKey ? 'HH:mm' : 'MM/dd HH:mm');
return time + ' · ' + event.getTitle();
});
if (!schedule.length) schedule.push('오늘 등록된 일정은 없습니다.');
var body = today + '\n\n오늘 일정\n' + schedule.map(function (s) { return '- ' + s; }).join('\n') +
'\n\n체크리스트\n' + DAILY_MAIL.checklist.map(function (s) { return '- ' + s; }).join('\n');
var htmlList = function (items) { return '<ul>' + items.map(function (text) { return '<li>' + escapeDailyHtml_(text) + '</li>'; }).join('') + '</ul>'; };
return {
to: address, dateKey: dateKey, skipToday: DAILY_MAIL.skipWeekends && (day === 0 || day === 6),
subject: '[오늘 일정] ' + today + ' · ' + events.length + '건',
body: body,
htmlBody: '<div style="font-family:Arial,sans-serif;line-height:1.7;color:#222"><h2>' + escapeDailyHtml_(today) +
'</h2><h3>오늘 일정</h3>' + htmlList(schedule) + '<h3>체크리스트</h3>' + htmlList(DAILY_MAIL.checklist) + '</div>'
};
}
function sendDailyMailMessage_(mail, isTest) {
if (MailApp.getRemainingDailyQuota() < 1) throw new Error('오늘 메일 발송 한도를 모두 사용했습니다. 나중에 다시 시도하세요.');
MailApp.sendEmail({ to: mail.to, subject: (isTest ? '[테스트] ' : '') + mail.subject, body: mail.body, htmlBody: mail.htmlBody });
var properties = PropertiesService.getScriptProperties();
if (properties.getProperty('DAILY_MAIL_SOURCE_SHEET_ID')) {
properties.setProperty(isTest ? 'DAILY_MAIL_LAST_TEST_REQUEST_AT' : 'DAILY_MAIL_LAST_AUTO_REQUEST_AT', new Date().toISOString());
}
}
function escapeDailyHtml_(value) {
return String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
// 파일을 여는 동작은 메뉴만 추가합니다. 승인·메일 발송·예약은 시작하기에서만 합니다.
function onOpen() {
SpreadsheetApp.getUi().createMenu('일정 메일')
.addItem('시작하기', 'startDailyMailFromSheet')
.addItem('설정 적용', 'applyDailyMailSettings')
.addSeparator()
.addItem('중지하기', 'stopDailyMailFromSheet')
.addItem('상태 확인', 'showDailyMailStatus')
.addToUi();
}
function startDailyMailFromSheet() {
return dailyMailSheetAction_(function (sheet, properties) {
saveDailyMailSettings_(sheet, properties);
setupDailyMail_();
updateDailyMailSheetStatus_(sheet, properties);
return '자동 발송을 시작했어요. 처음 시작할 때 본인에게 테스트 메일 한 통을 요청합니다. 받은편지함과 스팸함에서 실제 수신을 확인해 주세요. 다시 눌러도 테스트 메일이나 예약은 추가되지 않아요.';
});
}
function applyDailyMailSettings() {
return dailyMailSheetAction_(function (sheet, properties) {
var previous = properties.getProperty('DAILY_MAIL_SHEET_SETTINGS');
saveDailyMailSettings_(sheet, properties);
if (properties.getProperty('DAILY_MAIL_ENABLED') === 'true') {
try { ensureDailyTrigger_(properties); }
catch (error) {
// 새 예약 생성에 실패하면 이전의 내용 설정을 유지하고 성공으로 표시하지 않습니다.
if (previous) properties.setProperty('DAILY_MAIL_SHEET_SETTINGS', previous);
else properties.deleteProperty('DAILY_MAIL_SHEET_SETTINGS');
updateDailyMailSheetStatus_(sheet, properties);
throw new Error('설정 적용을 완료하지 못했어요. 입력한 값은 시트에 남아 있습니다. 잠시 후 설정 적용을 다시 눌러 주세요. 원인: ' + error.message);
}
}
updateDailyMailSheetStatus_(sheet, properties);
return properties.getProperty('DAILY_MAIL_ENABLED') === 'true'
? '설정을 적용했어요. 다음 예약부터 바뀐 설정으로 보내며 지금 테스트 메일을 추가로 보내지는 않아요.'
: '설정을 저장했어요. 메일은 아직 보내지 않아요. 원할 때 시작하기를 눌러 주세요.';
});
}
function stopDailyMailFromSheet() {
return dailyMailSheetAction_(function (sheet, properties) {
stopDailyMail_();
updateDailyMailSheetStatus_(sheet, properties);
return '자동 발송을 중지했어요. 설정과 캘린더 일정은 그대로 남아 있어요. 다시 사용하려면 시작하기를 눌러 주세요.';
});
}
function showDailyMailStatus() {
return dailyMailSheetAction_(function (sheet, properties) {
return updateDailyMailSheetStatus_(sheet, properties) + '\n시트 아래의 발송 설정과 최근 발송 요청도 갱신했어요. 실제 메일 수신 여부는 받은편지함에서 확인해 주세요.';
});
}
function dailyMailSheetAction_(action) {
// alert는 서버 실행을 일시 중지하므로 잠금을 해제한 뒤 표시합니다.
var result;
try {
result = withDailyMailLock_(function () {
var workbook = SpreadsheetApp.getActiveSpreadsheet();
if (!workbook) throw new Error('복사한 Google 시트를 PC 브라우저에서 열고 일정 메일 메뉴를 눌러 주세요.');
var sheet = workbook.getSheetByName('일정 메일');
if (!sheet) throw new Error('일정 메일 탭을 찾지 못했어요. 탭 이름을 일정 메일로 되돌리거나 원본 파일을 다시 복사해 주세요.');
var properties = PropertiesService.getScriptProperties();
bindDailyMailSheet_(workbook.getId(), properties);
try { return { ok: true, message: action(sheet, properties) }; }
catch (error) {
// 부분 실패도 화면에 남기되 다음 메뉴 실행으로 다시 시도할 수 있게 합니다.
try {
updateDailyMailSheetStatus_(sheet, properties);
sheet.getRange('B5').setValue('확인이 필요해요 · ' + error.message);
} catch (statusError) { console.log('상태 화면을 갱신하지 못했습니다: ' + statusError.message); }
throw error;
}
});
} catch (error) {
result = { ok: false, message: error.message || String(error) };
}
SpreadsheetApp.getUi().alert(result.ok ? '일정 메일' : '진행을 확인해 주세요', result.message, SpreadsheetApp.getUi().ButtonSet.OK);
return result;
}
function bindDailyMailSheet_(sheetId, properties) {
var savedId = properties.getProperty('DAILY_MAIL_SOURCE_SHEET_ID');
if (savedId !== sheetId) {
// 복사본에 원본의 실행 기록이 들어오더라도 새 사용자의 계정과 예약으로 시작합니다.
['DAILY_MAIL_OWNER', 'DAILY_MAIL_SETUP_TEST_SENT_TO', 'DAILY_MAIL_SENT_DATE',
'DAILY_MAIL_TRIGGER_ID', 'DAILY_MAIL_TRIGGER_HOUR', 'DAILY_MAIL_SHEET_SETTINGS',
'DAILY_MAIL_ENABLED', 'DAILY_MAIL_LAST_REQUEST_AT', 'DAILY_MAIL_LAST_TEST_REQUEST_AT', 'DAILY_MAIL_LAST_AUTO_REQUEST_AT'].forEach(function (key) { properties.deleteProperty(key); });
properties.setProperty('DAILY_MAIL_SOURCE_SHEET_ID', sheetId);
properties.setProperty('DAILY_MAIL_ENABLED', 'false');
}
var address = String(Session.getEffectiveUser().getEmail() || '').trim().toLowerCase();
if (!/^[^\s@,;]+@[^\s@,;]+\.[^\s@,;]+$/.test(address)) {
throw new Error('로그인한 계정의 이메일을 확인하지 못했어요. 개인 Google 계정으로 복사한 파일을 열어 주세요. 회사·학교 계정은 관리자 정책에 따라 제한될 수 있어요.');
}
var owner = properties.getProperty('DAILY_MAIL_OWNER');
if (owner && owner !== address) throw new Error('이 파일은 다른 계정에서 사용 중이에요. 내 드라이브에 파일을 복사한 뒤 내 계정으로 시작해 주세요.');
}
function saveDailyMailSettings_(sheet, properties) {
var hour = sheet.getRange('B8').getValue();
var frequency = String(sheet.getRange('B9').getValue()).trim();
var checklist = sheet.getRange('B13:B22').getValues().map(function (row) { return String(row[0]).trim(); }).filter(function (value) { return value !== ''; });
if (typeof hour !== 'number' || !Number.isInteger(hour) || hour < 0 || hour > 23) {
throw new Error('발송 시각(B8)에 0부터 23까지의 정수를 입력해 주세요. 예: 7은 한국 시간 오전 7~8시 사이예요.');
}
if (frequency !== '매일' && frequency !== '평일만') throw new Error('발송 요일(B9)에서 매일 또는 평일만을 선택해 주세요.');
if (!checklist.length || checklist.some(function (text) { return text.length > 200; })) {
throw new Error('체크리스트(B13:B22)에 200자 이내의 문장을 하나 이상 입력해 주세요. 사용하지 않는 줄은 비워 두세요.');
}
var settings = { sendHour: hour, skipWeekends: frequency === '평일만', checklist: checklist };
properties.setProperty('DAILY_MAIL_SHEET_SETTINGS', JSON.stringify(settings));
applyDailyMailConfig_(settings);
}
function loadDailyMailSettings_() {
var saved = PropertiesService.getScriptProperties().getProperty('DAILY_MAIL_SHEET_SETTINGS');
if (!saved) return;
try { applyDailyMailConfig_(JSON.parse(saved)); }
catch (error) { throw new Error('저장된 설정을 읽지 못했어요. 시트에서 설정 적용을 다시 눌러 주세요.'); }
}
function applyDailyMailConfig_(settings) {
DAILY_MAIL.sendHour = settings.sendHour;
DAILY_MAIL.skipWeekends = settings.skipWeekends;
DAILY_MAIL.checklist = settings.checklist;
}
function updateDailyMailSheetStatus_(sheet, properties) {
loadDailyMailSettings_();
var enabled = properties.getProperty('DAILY_MAIL_ENABLED') === 'true';
var count = dailyMailTriggers_().length;
var hasStarted = Boolean(properties.getProperty('DAILY_MAIL_SETUP_TEST_SENT_TO'));
var status = enabled
? (count === 1 ? '자동 발송 중' : '예약 확인이 필요해요 · 시작하기를 다시 눌러 주세요')
: (hasStarted ? '자동 발송 중지됨' : '아직 시작하지 않았어요');
var hour = DAILY_MAIL.sendHour;
var schedule = (DAILY_MAIL.skipWeekends ? '평일만' : '매일') + ' · 한국 시간 ' + hour + '~' + (hour + 1) + '시 사이';
sheet.getRange('B5').setValue(status);
sheet.getRange('B26').setValue(dailyMailRequestTime_(properties.getProperty('DAILY_MAIL_LAST_TEST_REQUEST_AT')));
sheet.getRange('B27').setValue((enabled ? '' : '중지 · ') + schedule);
sheet.getRange('B28').setValue(dailyMailRequestTime_(properties.getProperty('DAILY_MAIL_LAST_AUTO_REQUEST_AT')));
return status + ' · ' + schedule;
}
function dailyMailRequestTime_(iso) {
if (!iso) return '아직 발송 요청이 없어요';
return Utilities.formatDate(new Date(iso), 'Asia/Seoul', 'yyyy-MM-dd') + ' ' + Utilities.formatDate(new Date(iso), 'Asia/Seoul', 'HH:mm') + ' (한국 시간 · 발송 요청)';
}
function requireDailyMailPermissions_(withSheet) {
// 일부 권한만 승인한 환경에서는 예약을 만들기 전에 필요한 범위를 다시 요청합니다.
if (typeof ScriptApp.requireScopes !== 'function') return;
var scopes = [
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/script.send_mail',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/script.scriptapp'
];
if (withSheet) scopes.push('https://www.googleapis.com/auth/spreadsheets.currentonly');
ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, scopes);
}
- 확인할 결과
- 설정 적용 후 바꾼 시각·요일·체크리스트가 반영됩니다. 중지한 경우 정기 발송 예약이 해제됩니다.
기본 설정이 괜찮으면 이 단계는 건너뛰어도 됩니다.
바꾸고 싶거나, 막혔다면
강좌와 전체 코드를 한 번에 전달하세요. 새 AI 대화에 붙여넣은 뒤 원하는 변경이나 오류 메시지를 덧붙이면 됩니다. 기본 설정은 위 순서대로 끝내고 바로 사용할 수 있어요.
“체크리스트를 매장 오픈 업무에 맞게 써 줘.”
“시작하기를 눌렀는데 이 오류가 나와. …”
사용 방법과 전체 코드가 함께 복사됩니다. 개인 계정이나 일정은 포함되지 않습니다.
AI에게 전달되는 내용 보기
자주 묻는 질문
일정이 없거나 다른 캘린더에만 있어요.
일정이 없어도 빈 일정 안내와 체크리스트를 받습니다. 본인 기본 캘린더만 읽으므로 공유·구독·공휴일 캘린더의 일정은 기본 기능에 포함하지 않습니다.
위쪽에 ‘일정 메일’ 메뉴가 안 보여요.
PC 브라우저에서 자신이 복사한 파일을 열었는지 확인하고 새로고침하세요. 공개 원본이나 읽기 전용 화면에서는 시작할 수 없습니다. 모바일 앱을 기준으로 한 과정은 아닙니다.
Google 승인 화면이 다르거나 차단됐어요.
처음 실행할 때 “승인 필요”와 “Google에서 확인하지 않은 앱” 경고가 표시될 수 있습니다. 공개 앱 검증을 받은 프로그램이 아닌, 본인이 복사한 스크립트를 실행하는 과정입니다. 본인 개발자 계정과 필요한 접근 범위를 확인하세요. 다른 계정이 표시되거나 관리자가 차단했다면 그대로 진행하지 말고 실제 문구와 함께 도움을 요청하세요.
설정이 끝났는데 테스트 메일이 없어요.
사용한 계정의 받은편지함과 스팸함부터 확인하세요. 설정 완료는 발송 요청과 예약이 처리됐다는 뜻이며 실제 수신까지 확인한 것은 아닙니다. 계속 안 보이면 시작할 때 나온 메시지와 함께 AI에게 도움을 요청하세요. 파일을 계속 새로 복사할 필요는 없습니다.
다음날 메일이 안 오거나 두 통 왔어요.
설정 시트의 받을 시각에서 한 시간이 지났는지, 받을 요일을 평일만으로 바꾸지 않았는지 확인하세요. 두 통 온다면 이전에 복사한 다른 파일의 예약이 남아 있을 수 있습니다. 해당 파일에서 중지하기를 사용하세요.
AI에게 프롬프트를 꼭 보내야 하나요?
기본 기능은 이 페이지의 순서대로 시작하면 됩니다. 제공된 설정을 넘어 기능을 바꾸거나 문제를 해결하고 싶을 때 AI를 이용하세요. 프롬프트에는 강좌와 전체 코드가 담겨 있지만 본인 파일에 접근할 권한은 담겨 있지 않습니다.
파일 모아보기
이어서 해볼 것
설정을 마쳤다면 평소처럼 캘린더에 일정을 등록하며 사용하세요. 메일에 담을 문구는 설정 시트에서 바꿀 수 있습니다. 다음에는 업무 기록을 주간 보고서로 자동 정리해 보세요.