본문 바로가기
카테고리 없음

엑셀+구글 캘린더 연동으로 반복 업무 줄이기: 일정 생성·갱신·알림까지 자동화

by richjin7285 2025. 10. 22.

엑셀+구글 캘린더 연동 사진

팀에서 일정 관리를 엑셀로 시작하면 초반에는 편합니다. 누구나 열어 고치고, 필터로 정렬하고, 출력해서 공유하면 끝이니까요. 하지만 일정이 쌓이고 담당자가 바뀌기 시작하면 문제가 드러납니다. 엑셀에는 마감일을 D열에 바꿔 두고, 구글 캘린더에는 그 변경을 깜빡한 채로 두는 일이 반복되죠. 그러면 회의실은 예약돼 있는데 담당자는 모르는 상황, 출고일이 하루 당겨졌는데 물류팀은 알림을 못 받은 상황처럼 ‘사소하지만 치명적인’ 틈이 생깁니다. 결국 사람 손으로 두 곳을 함께 관리하는 구조 자체가 한계인 셈입니다.

해결책은 거창하지 않습니다. 엑셀을 “일정 데이터베이스”로, 구글 캘린더를 “표시/알림 장치”로 쓰고 둘을 연동하면 됩니다. 핵심은 “한 곳에서만 수정하고, 나머지는 자동으로 반영”되는 흐름을 만드는 것. 예를 들어 영업팀이 엑셀에서 고객 미팅 일정을 수정하면, 1) 구글 캘린더에 같은 일정이 즉시(혹은 주기적으로) 갱신되고, 2) 참석자에게 변경 알림이 날아가며, 3) 개인 휴대폰에도 동일하게 보이는 상태가 되는 겁니다. 사람이 반복 입력을 하지 않아도 일정의 ‘진실’이 항상 일치하니, 누락과 착오가 크게 줄어듭니다.

현장에서 잘 먹히는 패턴은 세 가지입니다. 첫째, 대량 초기 세팅은 엑셀을 CSV로 내보내 ICS로 변환해 한 번에 캘린더에 올립니다. 분기 교육 일정 30건, 프로젝트 마일스톤 50건처럼 “처음에 몰아서 넣는” 작업에 최적이죠. 둘째, 오피스 365 환경이라면 Power Automate로 엑셀 테이블에 행이 추가/수정될 때 구글 캘린더 이벤트를 자동 생성/업데이트합니다. 엑셀의 event_id 칼럼에 캘린더 이벤트 ID를 저장해 중복 없이 안정적으로 동기화할 수 있어요. 셋째, 구글 워크스페이스 중심이라면 Google Apps Script로 엑셀에서 공유하는 CSV를 주기적으로 읽어 캘린더와 동기화합니다. 15분 간격의 준실시간으로도 체감 효율이 큽니다.

이 연동이 좋은 이유는 단순히 시간을 아끼기 때문만이 아닙니다. 일정이 시스템에 들어오는 순간 표준화가 생기고, 그 표준 위에서 알림 규칙을 설계할 수 있기 때문이죠. 예를 들어 “모든 외부 회의는 시작 24시간 전과 2시간 전에 두 번 알림”, “출고 일정은 전일 16시 물류팀 채널로 집계 알림”, “월말 보고 회의는 팀 캘린더와 개인 캘린더에 동시 반영” 같은 규칙을 기술로 강제할 수 있습니다. 사람의 기억력 대신 시스템이 일관성을 보장하는 셈입니다.

물론 준비할 것도 있습니다. 엑셀 표에 title, start_date, start_time, end_date, end_time, location, description, all_day, status, event_id 정도의 표준 칼럼을 마련하고, 시간대는 Asia/Seoul로 통일해야 합니다. 반복 일정은 초기에는 “복제 행”으로 처리하고, 운영이 안정되면 RRULE을 써 정교하게 관리하면 됩니다. 무엇보다 중요한 건, “엑셀은 진실의 원천”이라는 팀 합의입니다. 진실이 한 곳에 있으면 나머지는 연결로 해결됩니다.

시작 전 1분 체크(복붙)
  • 표준 컬럼: title / start_date / start_time / end_date / end_time / location / description / all_day / status / event_id
  • 시간대: 엑셀·구글 캘린더·스크립트 모두 Asia/Seoul로 통일
  • 반복 일정: 초반엔 복제 행, 운영 안정 후 RRULE 적용
  • 연동 방식: CSV→ICS(대량 초기), Power Automate(완전 자동), Apps Script(준실시간)
  • 원칙: 수정은 엑셀 한 곳에서만, 캘린더는 표시/알림 전용

아래에서는 위 세 가지 루트를 단계별로 안내합니다. 처음엔 대량 등록으로 시간을 아끼고, 다음엔 자동 동기화로 반복 입력을 없애며, 마지막엔 알림 규칙으로 일정 품질을 끌어올려 보세요. 한 곳에서 고치면 어디서나 반영되는 리듬이 만들어지면, 회의/마감/출고가 제시간에 흐르고 팀의 하루는 확실히 편해집니다.

1) 가장 쉬운 루트: 엑셀 → CSV → ICS로 한 번에 캘린더 채우기

완전 자동화 전에 대량 초기 세팅부터 정리하면 체감 효과가 큽니다. 프로젝트 마일스톤 40개, 교육 커리큘럼 10주, 월말·주간 마감 일정 등 “벌크 등록”이 필요한 경우엔 엑셀을 표준 스키마로 정리 → CSV 저장 → ICS로 변환 → 구글 캘린더 가져오기 순서가 가장 간단하고 안전합니다. 처음부터 RRULE(반복 규칙)까지 쓰지 않아도 되고, 종일/시간형 일정도 깔끔히 반영됩니다.

1-1) 엑셀 표준 스키마(복붙 템플릿)

컬럼명 형식/예시 설명
title 월간 보고서 이벤트 제목
start_date 2025-11-01 YYYY-MM-DD(권장)
start_time 09:00 HH:MM(24시간제)
end_date 2025-11-01 종일일정은 시작일 다음날 권장(ICS 규칙)
end_time 10:00 HH:MM
location 회의실 A 선택
description 자료 제출 기한 포함 메모/링크 가능
all_day Y 또는 N 종일 여부
repeat_rule FREQ=WEEKLY;BYDAY=MO;COUNT=10 선택(RRULE, 고급)
calendar 팀 일정 여러 캘린더로 분배 시 사용

반복 일정은 초반엔 복제 행(주차별 행 생성)으로 처리하는 게 오류가 적습니다. 운영 안정 후 RRULE을 도입하세요.

1-2) CSV 저장 요령(인코딩/구분자)

  • 파일 형식: CSV UTF-8(콤마 구분). 한글/이모지 깨짐 방지
  • 날짜/시간: 셀 서식을 텍스트로 지정 후 YYYY-MM-DD, HH:MM 형태로 입력(엑셀이 2025-11-1 → 2025-11-01 자동 정규화되게)
  • 줄 바꿈: description에 줄 바꿈이 많으면 ICS 생성 시 이스케이프 필요(자동 스크립트 사용 권장)

1-3) CSV → ICS 변환 스크립트(간단·안정 버전)

CSV를 그대로 구글 캘린더로 가져와도 되지만, 종일 일정·반복 규칙·시간대 같은 세부 제어는 ICS가 더 안정적입니다. 아래 파이썬 스니펫은 Windows/Mac 어디서나 한 번 실행해 events.ics를 만들 수 있는 최소 예시입니다.

# pip 설치 불필요(표준 라이브러리만 사용). 파일: csv2ics.py
# CSV 컬럼: title,start_date,start_time,end_date,end_time,location,description,all_day,repeat_rule
import csv, datetime, uuid

def dtfmt(d, t):
    return datetime.datetime.strptime(f"{d} {t}", "%Y-%m-%d %H:%M").strftime("%Y%m%dT%H%M%S")

def esc(text):
    # ICS 특수문자 이스케이프(콤마, 세미콜론, 역슬래시, 줄바꿈)
    return (text or "").replace("\\","\\\\").replace(";","\\;").replace(",","\\,").replace("\n","\\n")

with open("events.csv", newline="", encoding="utf-8") as f, open("events.ics","w", encoding="utf-8") as o:
    r = csv.DictReader(f)
    o.write("BEGIN:VCALENDAR\nVERSION:2.0\nCALSCALE:GREGORIAN\nPRODID:-//Excel2ICS//EN\n")
    o.write("X-WR-TIMEZONE:Asia/Seoul\n")
    for row in r:
        uid = str(uuid.uuid4())
        title = esc(row.get("title",""))
        loc   = esc(row.get("location",""))
        desc  = esc(row.get("description",""))
        allday = (row.get("all_day","N").upper()=="Y")
        o.write("BEGIN:VEVENT\n")
        o.write(f"UID:{uid}\n")
        if allday:
            # 종일: DTEND는 종료 '다음날'이 표준(배타 범위)
            sd = row["start_date"].replace("-","")
            ed = row.get("end_date", row["start_date"]).replace("-","")
            o.write(f"DTSTART;VALUE=DATE:{sd}\n")
            o.write(f"DTEND;VALUE=DATE:{ed}\n")
        else:
            o.write(f"DTSTART;TZID=Asia/Seoul:{dtfmt(row['start_date'], row['start_time'])}\n")
            o.write(f"DTEND;TZID=Asia/Seoul:{dtfmt(row['end_date'], row['end_time'])}\n")
        o.write(f"SUMMARY:{title}\n")
        if loc:  o.write(f"LOCATION:{loc}\n")
        if desc: o.write(f"DESCRIPTION:{desc}\n")
        if row.get("repeat_rule"):
            # 예: FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10
            o.write(f"RRULE:{row['repeat_rule']}\n")
        o.write("END:VEVENT\n")
    o.write("END:VCALENDAR\n")
print("events.ics 생성 완료")
반복 규칙(RRULE) 예시
  • 매주 월/수 10회: FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10
  • 매월 말일: FREQ=MONTHLY;BYMONTHDAY=-1
  • 매년 3/1 종일: FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1

1-4) 구글 캘린더로 가져오기(Import)

  1. 구글 캘린더(웹) → 설정가져오기 & 내보내기 → 파일 선택(events.ics)
  2. 대상 캘린더 선택(팀 캘린더/개인 캘린더) → 가져오기
  3. 반복/종일/시간대가 올바르게 반영됐는지 샘플 몇 건 확인

대량 수정 시엔 ICS를 재생성해 새 캘린더로 임포트 → 기존 캘린더와 병행 검수 후 교체하면 안전합니다.

1-5) 자주 생기는 오류 & 빠른 처방

증상 원인 해결
한글/이모지 깨짐 CSV/ICS 인코딩 문제 CSV UTF-8로 저장, ICS 파일도 UTF-8 유지
종일 일정 하루 짧게 표시 ICS 종일 종료일은 ‘배타적’ DTEND를 다음날 날짜로 설정(예: 3/1 종일 → DTEND=3/02)
시간이 틀어짐 시간대 미지정 DTSTART/DTEND에 TZID=Asia/Seoul 지정
반복 일정 일부만 생성 RRULE 오타/기간 충돌 RRULE 문법 재확인, BYDAY/COUNT/UNTIL 조합 점검

1-6) 운영 팁(가독성·분류·검수 루틴)

  • 캘린더 분리: “팀 일정/외부 미팅/출고 일정”을 별도 캘린더로 분리 후 색상 규칙 적용
  • 태그 관례: 제목 접두사 [보고], [교육], [출고] 사용 → 검색/필터 용이
  • 검수: 임포트 전 소규모 샘플(5~10건) 검증 → 전체 임포트

요약하면, 표준 스키마로 엑셀을 정리하고 CSV→ICS 한 번만 통과해도 수십~수백 건의 일정을 정확하게 캘린더로 옮길 수 있습니다. 이 루트로 초기 깔끔한 기반을 만든 뒤, 다음 섹션의 Power Automate 또는 Apps Script수정·추가·삭제까지 자동화하면 “한 곳에서 고치면 어디서나 반영”되는 리듬이 완성됩니다.

2) 완전 자동: Power Automate로 엑셀(OneDrive/SharePoint) ↔ 구글 캘린더 동기화

구글 캘린더 동기화 사진

오피스 365를 쓰고 있다면 Power Automate(파워 오토메이트)로 “엑셀 행 추가/수정/삭제 → 구글 캘린더 생성/업데이트/삭제”까지 완전 자동화를 만들 수 있습니다. 핵심은 엑셀을 ‘진실의 원천’으로 두고, 구글 캘린더는 표시/알림 장치로 운용하는 것. 아래 가이드는 테이블 설계 → 플로우 만들기 → 중복 방지(Upsert) → 반복/종일 처리 → 오류/로그 → 운영 팁 순서로 정리했습니다.

2-1) 준비물 & 표(Table) 설계

  • 저장 위치: OneDrive 또는 SharePoint(팀 추천)
  • 엑셀 파일: 반드시 삽입 → 표테이블화(열 머리글 포함)
  • 열(표준): title, start_date(YYYY-MM-DD), start_time(HH:MM), end_date, end_time, all_day(Y/N), location, description, status(Active/Cancelled), calendar_id(선택), event_id(구글 이벤트 ID 저장)
컬럼 형식/예시 메모
title 월간 보고 이벤트 제목
start_date / start_time 2025-11-01 / 09:00 텍스트 권장(자동 변환 방지)
end_date / end_time 2025-11-01 / 10:00 종일이면 시간 비움
all_day Y 또는 N 종일 여부
status Active / Cancelled 삭제 대신 취소 권장
calendar_id team@group.calendar.google.com 여러 캘린더 분배 시
event_id (자동 채움) 구글 이벤트 ID 저장(Upsert용)

2-2) 플로우 만들기(핵심 단계)

  1. 트리거: When a row is added, modified or deleted (Excel Online Business)
  2. 조건:
    • RowChangeType = Insert 또는 Update 일 때만 계속
    • Status = 'Active'인 행만 대상(취소는 별도 분기)
  3. 분기(Condition): event_id가 비었으면 → Create an event, 값이 있으면 → Update an event
  4. 작성 후: 생성 응답의 id를 엑셀의 event_id 칼 럼에 Update a row로 저장
  5. 취소 분기: Status = 'Cancelled'이고 event_id가 있으면 → Delete an event
연결 — Power Automate에서 Google Calendar 커넥터를 추가해 계정 인증(편집 권한 필요). Excel 커넥터는 테이블로 만든 시트만 인식합니다.

2-3) 필드 매핑(표준 표현식 포함)

  • Title = @{items('변경_행')['title']}
  • All-day event = @{equals(toUpper(items('변경_행')['all_day']), 'Y')}
  • Start = @{formatDateTime(concat(items('변경_행')['start_date'], ' ', coalesce(items('변경_행')['start_time'], '00:00')), 'yyyy-MM-ddTHH:mm:00')}
  • End = @{if(equals(toUpper(items('변경_행')['all_day']), 'Y'), formatDateTime(addDays(items('변경_행')['end_date'],1), 'yyyy-MM-ddT00:00:00'), formatDateTime(concat(items('변경_행')['end_date'], ' ', items('변경_행')['end_time']), 'yyyy-MM-ddTHH:mm:00'))}
  • Time Zone = Korea Standard Time (또는 Asia/Seoul, 커넥터 옵션에 맞춰 선택)
  • Location / Description = 각각 열 매핑
  • Calendar Id = 고정값 또는 calendar_id 칼럼

종일 일정은 ICS와 마찬가지로 종료일의 다음날 00:00으로 지정해야 하루가 꽉 차게 보입니다.

2-4) Upsert 패턴(중복 방지·수정/삭제 처리)

  • Create: event_id가 비었을 때만 생성 → 응답 id를 엑셀에 저장
  • Update: event_id가 있을 때 → Update an event로 시간/제목/장소/메모 갱신
  • Cancel: Status='Cancelled' & event_id 존재 → Delete an eventevent_id 비우기(또는 ‘취소됨’ 기록)

2-5) 반복/종일/리마인더

  • 종일: 위 매핑처럼 All-day=true + End=다음날 00:00
  • 반복:
    • 초기엔 복제 행(매주 월 10회 → 10개 행)으로 간단히 운영
    • 고급: repeat_rule에 RRULE 저장 & “Create event (advanced)” 같은 옵션으로 전달(커넥터 지원 여부 확인)
  • 리마인더: 구글 캘린더 기본(10분) 사용 또는 커넥터의 Reminders 옵션으로 분/메일/팝업 지정

2-6) 오류 처리/로그(운영 내구성)

  • Scope 액션으로 Try / Catch 분리 → Configure run after에서 실패 시 분기
  • 로그 테이블(엑셀 logs): ts, row_id, op(create/update/delete), result(ok/fail), note 기록
  • 경보: 실패 발생 시 Teams/메일 알림(요약: 행 PK, 오류 메시지)

2-7) 권한/성능/유지보수 팁

  • 권한: 플로우 소유 계정은 대상 구글 캘린더 편집 권한 필요(공유 설정에서 초대)
  • 파일 잠금: Excel Online은 동시 편집 중 행 인식이 지연될 때가 있습니다. 대량 변경은 배치 시간에 몰아서 처리 권장
  • 대량 생성: 수백 건 이상은 CSV→ICS 임포트로 초기 이관 → Power Automate는 변경분 관리용으로
  • 버전: 열 이름/형식 변경 시 플로우의 매핑 재저장 필수(깨짐 방지)
운영 체크리스트(복붙)
  • 테이블 열 = title/start_date/start_time/end_date/end_time/all_day/status/event_id 최소 보장
  • 시간대 통일: Korea Standard Time(또는 Asia/Seoul)
  • Upsert 규칙: event_id 비면 Create, 있으면 Update, Cancelled면 Delete
  • 로그 남기기: 성공/실패/변경 필드 요약

요약하면, Power Automate로 엑셀 변경 = 캘린더 자동 반영 흐름을 만들면 반복 입력이 사라집니다. 초기 대량 등록은 1번 섹션의 CSV→ICS로, 운영 변경은 Power Automate Upsert로, 세밀 제어가 필요하면 3번 섹션의 Apps Script로 보완하세요. 한 곳(엑셀)에서만 고치고, 팀 전체는 캘린더로 확인하는 리듬이 잡히면 누락과 일정 충돌이 극적으로 줄어듭니다.

3) 준실시간 동기화: Google Apps Script로 엑셀(CSV) ↔ 구글 캘린더 연결

오피스 365/사내 서버에 있는 엑셀을 CSV로 공유할 수 있다면, 구글 쪽에서는 Google Apps Script 하나로 준실시간(예: 15분 간격) 동기화를 안정적으로 운영할 수 있습니다. 구조는 간단합니다. 엑셀(원본) → 공개 CSV URL → 스프레드시트 수신(events_raw) → 캘린더(생성/업데이트/삭제). 핵심은 event_idUpsert(중복 방지)하고, Status로 삭제/취소를 제어하는 것입니다. 또한 종일/시간 일정, 반복(RRULE/시리즈), 시간대, 실패 로그까지 한 번에 설계해야 현장에서 흔들리지 않습니다.

3-1) 스프레드시트 준비(수신 버퍼)

  • 스프레드시트에 시트 2개 생성: events_raw(수신), _logs(로그)
  • events_raw 헤더 예시: title, start_date, start_time, end_date, end_time, all_day(Y/N), location, description, status(Active/Cancelled), event_id, repeat_rule, calendar_id
  • _logs 헤더: ts, level, op, row, message

3-2) CSV 수신(덮어쓰기) — 인코딩/구분자 대응

const CSV_URL = 'https://.../events.csv'; // 엑셀에서 주기적 내보내기/공유 링크
function fetchCsvToSheet() {
  const sh = SpreadsheetApp.getActive().getSheetByName('events_raw');
  const res = UrlFetchApp.fetch(CSV_URL, {followRedirects:true, muteHttpExceptions:true});
  if (res.getResponseCode() !== 200) return log_('ERROR','fetch',0,'HTTP '+res.getResponseCode());
  const txt = res.getContentText('UTF-8') || res.getContentText(); // 기본 UTF-8
  const rows = Utilities.parseCsv(txt); // 콤마 구분
  if (!rows.length) return log_('WARN','fetch',0,'Empty CSV');
  sh.clearContents();
  sh.getRange(1,1,rows.length, rows[0].length).setValues(rows);
  log_('INFO','fetch',rows.length-1,'rows imported');
}
function log_(level, op, row, msg){
  const sh = SpreadsheetApp.getActive().getSheetByName('_logs');
  sh.appendRow([new Date(), level, op, row, msg]);
}

Tip. CSV가 세미콜론(;) 일 땐 split 후 수동 파싱하거나, 엑셀 내보내기를 콤마·UTF-8로 통일하세요.

3-3) 캘린더 동기화(Upsert: 생성/업데이트/취소) — 종일/시간형 함께 처리

const CAL_FALLBACK = 'your_calendar_id@group.calendar.google.com'; // 기본 캘린더

function syncToCalendar(){
  const sh = SpreadsheetApp.getActive().getSheetByName('events_raw');
  const data = sh.getDataRange().getValues();
  if (data.length <= 1) return log_('WARN','sync',0,'no rows');
  const header = data[0], idx = Object.fromEntries(header.map((h,i)=>[h,i]));
  const n = data.length;

  for (let r=1; r<n; r++){
    const row = data[r];
    const title = (row[idx.title]||'').toString().trim();
    if (!title) { log_('WARN','skip',r+1,'no title'); continue; }

    const allDay = (row[idx.all_day]||'N').toString().toUpperCase()==='Y';
    const status = (row[idx.status]||'Active').toString();
    const calId  = (row[idx.calendar_id]||CAL_FALLBACK).toString();
    const cal    = CalendarApp.getCalendarById(calId);
    if (!cal){ log_('ERROR','calendar',r+1,'invalid calendar_id'); continue; }

    const startStr = (row[idx.start_date]||'').toString();
    const endStr   = (row[idx.end_date]||startStr).toString();
    const stTime   = (row[idx.start_time]||'00:00').toString();
    const enTime   = (row[idx.end_time]||'00:00').toString();

    let start, end;
    try {
      start = allDay ? new Date(startStr) : new Date(startStr+'T'+stTime+':00');
      // 종일 끝은 '배타적' — 다음날 00:00가 규칙
      end   = allDay ? new Date(new Date(endStr).getTime()+24*60*60*1000)
                     : new Date(endStr+'T'+enTime+':00');
    } catch(e){
      log_('ERROR','date-parse',r+1,e.message); continue;
    }

    const desc = (row[idx.description]||'').toString();
    const loc  = (row[idx.location]||'').toString();
    const rrule = (row[idx.repeat_rule]||'').toString().trim();
    let eventId = (row[idx.event_id]||'').toString();

    try{
      if (status==='Cancelled' && eventId){
        const ev = cal.getEventById(eventId);
        if (ev) ev.deleteEvent();
        sh.getRange(r+1, idx.event_id+1).setValue('');
        log_('INFO','delete',r+1,'cancelled');
        continue;
      }

      // 반복 규칙이 있으면 '시리즈'로, 아니면 단일 이벤트
      if (!eventId){
        let ev;
        if (rrule){
          // RRULE 문자열을 Recurrence로 변환하는 대신, 기본 패턴은 newRecurrence로 직접 생성 권장
          // rrule을 그대로 쓰려면 CSV에서 BYDAY/BYMONTHDAY/COUNT 등 제한된 패턴으로 관리
          const rec = buildRecurrenceFromRule_(rrule, start, end);
          if (!rec){ log_('ERROR','rrule',r+1,'invalid repeat_rule'); continue; }
          ev = cal.createEventSeries(title, start, end, rec, {description:desc, location:loc});
        } else {
          ev = allDay
            ? cal.createAllDayEvent(title, start, end, {description:desc, location:loc})
            : cal.createEvent(title, start, end, {description:desc, location:loc});
        }
        sh.getRange(r+1, idx.event_id+1).setValue(ev.getId());
        log_('INFO','create',r+1,(rrule?'series':'single'));
      } else {
        // 업데이트
        const ev = cal.getEventById(eventId);
        if (!ev){
          // 없으면 재생성
          const nev = allDay
            ? cal.createAllDayEvent(title, start, end, {description:desc, location:loc})
            : cal.createEvent(title, start, end, {description:desc, location:loc});
          sh.getRange(r+1, idx.event_id+1).setValue(nev.getId());
          log_('WARN','recreate',r+1,'missing old event');
        } else {
          if (allDay) ev.setAllDayDates(start, end); else ev.setTime(start, end);
          ev.setTitle(title); ev.setDescription(desc); ev.setLocation(loc);
          log_('INFO','update',r+1,'ok');
        }
      }
    }catch(e){
      log_('ERROR','sync',r+1,e.message);
    }
  }
}

/** 간단 RRULE 파서 → Recurrence(일부 패턴만) */
function buildRecurrenceFromRule_(rrule, start, end){
  const parts = Object.fromEntries(rrule.split(';').map(s=>s.split('=')));
  const rec = CalendarApp.newRecurrence();
  const freq = (parts.FREQ||'').toUpperCase();
  const count = parts.COUNT ? parseInt(parts.COUNT,10) : null;
  const until = parts.UNTIL ? new Date(parts.UNTIL.replace(/-/g,'/')) : null;
  if (count) rec.addDailyRule().times(count); // 임시 초기화
  switch(freq){
    case 'DAILY':
      let dr = rec.addDailyRule();
      if (count) dr.times(count);
      if (until) dr.until(until);
      break;
    case 'WEEKLY':
      let wr = rec.addWeeklyRule();
      if (parts.BYDAY){
        const map = {MO:CalendarApp.Weekday.MONDAY, TU:CalendarApp.Weekday.TUESDAY, WE:CalendarApp.Weekday.WEDNESDAY,
                     TH:CalendarApp.Weekday.THURSDAY, FR:CalendarApp.Weekday.FRIDAY, SA:CalendarApp.Weekday.SATURDAY, SU:CalendarApp.Weekday.SUNDAY};
        parts.BYDAY.split(',').forEach(d => wr.onlyOnWeekdays(map[d]));
      }
      if (count) wr.times(count);
      if (until) wr.until(until);
      break;
    case 'MONTHLY':
      let mr = rec.addMonthlyRule();
      if (parts.BYMONTHDAY) mr.onMonthDay(parseInt(parts.BYMONTHDAY,10));
      if (count) mr.times(count);
      if (until) mr.until(until);
      break;
    case 'YEARLY':
      let yr = rec.addYearlyRule();
      if (count) yr.times(count);
      if (until) yr.until(until);
      break;
    default:
      return null;
  }
  return rec;
}

주의. CalendarApp.createEventSeries()시리즈 전체로 취급되어 부분 수정이 어렵습니다. 현장에선 반복일정도 “복제 행”으로 관리하는 편이 유지보수에 유리합니다(행 단위로 취소/수정 가능).

3-4) 시간대/서머타임/포맷—‘시간 틀어짐’ 방지 체크

  • 프로젝트 설정 → 시간대: Asia/Seoul
  • 스프레드시트 파일 설정 시간대도 서울로 통일
  • CSV의 날짜/시간은 YYYY-MM-DD, HH:MM (24시간제)로 고정
  • 종일 일정: end_date = 시작일로 전달하되, 코드에서 다음날 00:00까지 자동 변환

3-5) 검증/무결성 가드레일(실패해도 ‘어디가 문제인지’ 보이게)

검사 내용 대응
필수값 title, start_date 없으면 _logs WARN 남기고 스킵
날짜 파싱 유효 날짜/시간 여부 파싱 실패 시 ERROR 로깅
캘린더 권한 calendar_id 접근/편집 권한 없으면 ERROR, 기본 캘린더로 대체 금지

3-6) 트리거/운영 SOP(복붙)

[트리거]
- 15분 간격: fetchCsvToSheet → syncToCalendar 순서 실행
- 매일 09:00: 로그 요약 메일(실패/경고 건수) 발송(선택)

[운영]
- CSV 내보내기 규칙: UTF-8, 콤마, 헤더 고정
- event_id는 사용자가 직접 수정하지 않기(동기화 키)
- 부분 중단 시 _logs에서 row, message 보고 해당 행만 재수정

3-7) 고급 팁 — 다중 캘린더/참석자/리마인더

  • 다중 캘린더: calendar_id를 열로 받아 팀/개인 등 분배 가능
  • 참석자: createEventev.addGuest(email) 반복으로 추가(동의 메일 자동 발송)
  • 리마인더: ev.addPopupReminder(minutes), ev.addEmailReminder(minutes) 조합
  • 색상: ev.setColor(CalendarApp.EventColor.PALE_BLUE) 등으로 유형별 색상 규칙

정리하면, Apps Script 기반 동기화는 비용이 거의 들지 않으면서도 준실시간에 가깝게 돌아갑니다. event_id Upsert, Status로 취소, 시간대/종일 규칙, 로그 가시화만 지키면 “엑셀에서 한 번 수정 → 캘린더 어디서나 반영”의 리듬을 안정적으로 유지할 수 있습니다. 초기엔 반복도 복제 행으로, 운영이 자리 잡으면 시리즈 생성으로 점진 확장하세요.

 

결론: “수정은 엑셀 한 곳, 확인은 캘린더 어디서나” — 반복 입력을 없애는 가장 현실적인 루틴

수정은 엑셀 한 곳, 확인은 캘린더 어디서나 사진

엑셀과 구글 캘린더를 잇는 목적은 멋진 자동화가 아니라 실수와 중복을 줄이고, 팀의 시간을 되돌려 받는 것입니다. 초기에는 CSV→ICS로 수십 건의 일정을 한 번에 올려 기반을 깔고, 운영 단계에서는 Power Automate로 행 변경=즉시 반영 흐름을 만들며, 세밀 제어가 필요하면 Apps Script로 준실시간 동기화까지 확장하세요. 이때 반드시 지켜야 할 원칙은 세 가지입니다. 첫째, 표준 스키마(title, date/time, all_day, status, event_id)로 엑셀을 정리한다. 둘째, Upsert(event_id 기반)로 중복 없이 생성/수정/취소를 일관되게 처리한다. 셋째, 시간대 통일(Asia/Seoul)과 종일 규칙(끝=다음날 00:00), 로그 가시화로 운영 내구성을 확보한다. 이 세 가지를 갖추면, “두 곳에 따로 적고 따로 고치던” 반복 업무가 사라지고, 캘린더는 항상 최신의 진실을 보여줍니다.

이번 주 실행 체크리스트(복붙)
  • 엑셀 표준 컬럼 확정: title/start_date/start_time/end_date/end_time/all_day/status/event_id
  • 대량 초기 등록: CSV → ICS로 임포트(샘플 10건 검수 후 전체 반영)
  • 완전 자동화: Power Automate 플로우(Insert/Update/Cancelled 분기 + event_id 저장)
  • 준실시간 보완: Apps Script(15분 간격 fetch → sync, 오류 시 로그/메일 알림)
  • 규칙 고정: 외부 미팅 24시간·2시간 전 알림, 출고 일정 전일 16시 집계 알림

결국 핵심은 “단일 수정·자동 반영”입니다. 담당자가 바뀌어도, 일정이 몰려도, 시스템은 같은 규칙으로 같은 품질의 결과를 내야 합니다. 엑셀 한 곳에서 일정이 움직이면 캘린더가 따라오고, 모바일 알림이 제시간에 울리는 상태—그 순간, 팀의 하루는 예측 가능해지고 회의/마감/출고의 실패 비용이 줄어듭니다. 오늘은 표준 스키마와 CSV→ICS부터, 내일은 Power Automate 엽 서트, 이번 주 안에 Apps Script 트리거까지 얹어 보세요. 반복 입력이 사라진 자리만큼, 팀은 더 중요한 일에 시간을 쓸 수 있습니다.