텔레그램 봇으로 Google Calendar 일정 관리하기

2026. 7. 25. 16:09파이썬

등록·조회·수정·삭제·음성 입력 기능으로 업그레이드

목차

  1. 업그레이드 버전의 목표
  2. 전체 시스템 구조
  3. 휴대전화 음성 입력이 바로 동작하는 이유
  4. 업그레이드 버전에서 지원할 명령
  5. 사용자 의도 분석
  6. 문장에서 추출할 정보
  7. Google Calendar API 기능 연결
  8. 일정 등록 기능
  9. 일정 조회 기능
  10. 일정 수정 기능
  11. 일정 삭제 기능
  12. 왜 이벤트 ID가 필요한가?
  13. 여러 일정이 검색될 때 처리
  14. 실행 전 확인 기능
  15. 대기 중인 작업 저장
  16. 메시지 처리 중심 함수
  17. 권장 프로젝트 구조
  18. 업그레이드 단계
  19. 테스트 시나리오
  20. 보안상 추가해야 할 기능
  21. 업그레이드 버전의 핵심

1. 업그레이드 버전의 목표

기존 버전에서는 다음처럼 입력하면 무조건 일정을 등록했습니다.

내일 오후 3시 프로젝트 회의

업그레이드 버전에서는 다음 기능까지 지원합니다.

일정 등록
일정 조회
일정 수정
일정 삭제
휴대전화 음성 입력
실행 전 확인
여러 일정이 검색될 때 선택

목표: 별도의 Calendar 앱을 열지 않고 텔레그램에서 자연어로 일정을 관리하는 것.

일정 등록 예시

사용자: 내일 오후 3시 프로젝트 회의 등록해줘

봇: 다음 일정으로 등록할까요?
제목: 프로젝트 회의
시간: 2026-07-26 15:00~16:00
[등록] [취소]

일정 조회 예시

사용자: 내일 일정 알려줘

봇: 2026-07-26 일정입니다.
1. 10:00 병원
2. 15:00 프로젝트 회의
3. 19:00 저녁 약속

일정 수정 예시

사용자: 내일 오후 3시 프로젝트 회의를 오후 4시로 변경해줘

봇: 다음 일정을 수정할까요?
기존: 15:00 프로젝트 회의
변경: 16:00 프로젝트 회의
[수정] [취소]

일정 삭제 예시

사용자: 내일 오후 4시 프로젝트 회의 삭제해줘

봇: 다음 일정을 삭제할까요?
2026-07-26 16:00 프로젝트 회의
[삭제] [취소]

2. 전체 시스템 구조

텔레그램 모바일 앱
        ↓
텍스트 또는 음성 입력
        ↓
Telegram Bot API
        ↓
Python bot.py
        ↓
사용자 의도 분석
        ↓
등록 / 조회 / 수정 / 삭제 구분
        ↓
Google Calendar API
        ↓
처리 결과를 텔레그램으로 응답

기존 버전 업그레이드 버전

처리 방식 모든 메시지를 등록 요청으로 처리 명령 종류 판단 → 정보 추출 → 사용자 확인 → API 실행

3. 휴대전화 음성 입력이 바로 동작하는 이유

음성으로 일정을 입력하는 방법은 두 가지입니다.

3.1 키보드 마이크를 사용한 경우

음성 → 휴대전화 음성 인식 → 텍스트 변환 → 텔레그램으로 텍스트 전송

봇이 받는 메시지는 일반 텍스트이므로, 기존 MessageHandler 코드만으로 처리할 수 있습니다.

MessageHandler(
    filters.TEXT & ~filters.COMMAND,
    handle_message,
)

→ 별도의 음성 인식 기능을 추가하지 않아도 됩니다.

3.2 텔레그램 음성 메시지를 보낸 경우

채팅창 마이크를 길게 눌러 보내는 것은 텍스트가 아니라 음성 메시지입니다.

사용자 음성 녹음 → 텔레그램 음성 파일 전송 → 봇이 음성 파일 수신

이 경우 텍스트 Handler로는 처리되지 않으며, voice.file_id로 파일을 내려받아 별도의 음성인식으로 변환해야 합니다.

처리 구조

텔레그램 음성 메시지
        ↓
voice.file_id 확인
        ↓
음성 파일 다운로드
        ↓
Speech-to-Text 처리
        ↓
텍스트 변환
        ↓
일정 명령 분석

Handler 등록

application.add_handler(
    MessageHandler(
        filters.VOICE,
        handle_voice,
    )
)

핵심 코드

async def handle_voice(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None or update.message.voice is None:
        return

    voice = update.message.voice

    telegram_file = await context.bot.get_file(
        voice.file_id
    )

    await telegram_file.download_to_drive(
        "temp_voice.ogg"
    )

    # 음성인식 프로그램으로 텍스트 변환
    recognized_text = speech_to_text(
        "temp_voice.ogg"
    )

    await process_calendar_command(
        update,
        context,
        recognized_text,
    )

교육 초기 버전에서는 휴대전화 키보드 음성 입력을 사용하는 편이 간단합니다. 실제 텔레그램 음성 메시지 처리는 별도의 확장 실습으로 분리하는 것이 좋습니다.


4. 업그레이드 버전에서 지원할 명령

사용자 문장에서 네 가지 의도를 구분합니다.

CREATE  일정 등록
READ    일정 조회
UPDATE  일정 수정
DELETE  일정 삭제

구분 표현 예시

등록 내일 오후 3시 회의 / 내일 오후 3시 회의 등록해줘 / 다음 주 월요일 오전 10시 병원 일정 추가해줘 / 8월 3일 오후 2시 고객 미팅 잡아줘
조회 오늘 일정 알려줘 / 내일 일정 보여줘 / 이번 주 일정 조회해줘 / 다음 주 월요일 회의 찾아줘
수정 내일 오후 3시 회의를 4시로 변경해줘 / 금요일 병원 일정을 오전 11시로 바꿔줘 / 프로젝트 회의 제목을 주간회의로 수정해줘
삭제 내일 오후 4시 회의 삭제해줘 / 금요일 병원 일정 취소해줘 / 다음 주 월요일 주간회의 지워줘

5. 사용자 의도 분석

메시지를 받으면 가장 먼저 명령 종류를 판단합니다.

from enum import Enum


class Intent(str, Enum):
    CREATE = "create"
    READ = "read"
    UPDATE = "update"
    DELETE = "delete"
    HELP = "help"
    UNKNOWN = "unknown"

간단한 규칙 기반 판단

def detect_intent(text: str) -> Intent:
    normalized = text.strip().lower()

    if any(
        word in normalized
        for word in ["삭제", "지워", "취소"]
    ):
        return Intent.DELETE

    if any(
        word in normalized
        for word in ["수정", "변경", "바꿔"]
    ):
        return Intent.UPDATE

    if any(
        word in normalized
        for word in ["조회", "보여줘", "알려줘", "찾아줘"]
    ):
        return Intent.READ

    if any(
        word in normalized
        for word in ["등록", "추가", "잡아줘"]
    ):
        return Intent.CREATE

    # 날짜와 시간이 있으면 등록 요청으로 판단할 수 있습니다.
    if contains_date_and_time(normalized):
        return Intent.CREATE

    return Intent.UNKNOWN

판단 예시

내일 오후 3시 회의        → CREATE
내일 일정 알려줘          → READ
내일 회의를 4시로 변경해줘 → UPDATE
내일 회의 삭제해줘        → DELETE

6. 문장에서 추출할 정보

의도 판단만으로는 API를 호출할 수 없습니다. 다음 정보를 추가로 추출해야 합니다.

명령 종류
날짜
시작 시간
종료 시간
일정 제목
수정 전 정보
수정 후 정보

데이터 객체로 관리

from dataclasses import dataclass
from datetime import datetime
from typing import Optional


@dataclass
class CalendarCommand:
    intent: Intent

    title: Optional[str] = None

    start: Optional[datetime] = None
    end: Optional[datetime] = None

    original_title: Optional[str] = None
    original_start: Optional[datetime] = None

    new_title: Optional[str] = None
    new_start: Optional[datetime] = None
    new_end: Optional[datetime] = None

등록 요청 분석 예

입력: 내일 오후 3시 프로젝트 회의

결과:
intent: CREATE
title: 프로젝트 회의
start: 2026-07-26 15:00
end: 2026-07-26 16:00

수정 요청 분석 예

입력: 내일 오후 3시 프로젝트 회의를 오후 4시로 변경해줘

결과:
intent: UPDATE
original_title: 프로젝트 회의
original_start: 2026-07-26 15:00
new_title: 프로젝트 회의
new_start: 2026-07-26 16:00
new_end: 2026-07-26 17:00

7. Google Calendar API 기능 연결

기능 API

등록 events.insert
조회 events.list
수정 events.patch 또는 events.update
삭제 events.delete

update는 이벤트 전체를 교체하고, patch는 일부 필드만 변경합니다. 삭제는 이벤트 ID를 지정해 실행합니다.

필요한 OAuth 권한

SCOPES = [
    "https://www.googleapis.com/auth/calendar.events"
]

calendar.events 범위는 이벤트 조회·생성·수정·삭제 권한을 모두 포함합니다.


8. 일정 등록 기능

기존 버전에서 사용한 기능입니다.

def create_event(
    service,
    title: str,
    start: datetime,
    end: datetime,
) -> dict:
    event_body = {
        "summary": title,
        "start": {
            "dateTime": start.isoformat(),
            "timeZone": "Asia/Seoul",
        },
        "end": {
            "dateTime": end.isoformat(),
            "timeZone": "Asia/Seoul",
        },
    }

    return (
        service.events()
        .insert(
            calendarId="primary",
            body=event_body,
        )
        .execute()
    )

primary는 현재 인증한 사용자의 기본 캘린더를 의미합니다.


9. 일정 조회 기능

수정·삭제 전에는 먼저 해당 일정을 찾아야 합니다.

def search_events(
    service,
    start: datetime,
    end: datetime,
    keyword: str | None = None,
) -> list[dict]:
    request = service.events().list(
        calendarId="primary",
        timeMin=start.isoformat(),
        timeMax=end.isoformat(),
        singleEvents=True,
        orderBy="startTime",
        q=keyword,
    )

    result = request.execute()

    return result.get("items", [])

파라미터 역할

timeMin / timeMax 조회 기간 제한
q 제목·설명·위치 텍스트 검색
singleEvents 반복 일정을 개별 항목으로 확장
orderBy 시작 시간순 정렬

사용 예

events = search_events(
    service=service,
    start=day_start,
    end=day_end,
    keyword="프로젝트 회의",
)

응답 예

2026-07-26 일정입니다.
1. 10:00 병원
2. 15:00 프로젝트 회의
3. 19:00 저녁 약속

10. 일정 수정 기능

수정하려면 먼저 검색 결과에서 eventId를 확보해야 합니다.

일정 검색 → 일치하는 이벤트 확인 → eventId 확보 → 변경 내용 구성 → events.patch 실행

시간만 변경

def update_event_time(
    service,
    event_id: str,
    new_start: datetime,
    new_end: datetime,
) -> dict:
    body = {
        "start": {
            "dateTime": new_start.isoformat(),
            "timeZone": "Asia/Seoul",
        },
        "end": {
            "dateTime": new_end.isoformat(),
            "timeZone": "Asia/Seoul",
        },
    }

    return (
        service.events()
        .patch(
            calendarId="primary",
            eventId=event_id,
            body=body,
        )
        .execute()
    )

제목과 시간 모두 변경

def update_event(
    service,
    event_id: str,
    title: str,
    start: datetime,
    end: datetime,
) -> dict:
    body = {
        "summary": title,
        "start": {
            "dateTime": start.isoformat(),
            "timeZone": "Asia/Seoul",
        },
        "end": {
            "dateTime": end.isoformat(),
            "timeZone": "Asia/Seoul",
        },
    }

    return (
        service.events()
        .patch(
            calendarId="primary",
            eventId=event_id,
            body=body,
        )
        .execute()
    )

update는 이벤트 전체 리소스를 갱신하고, patch는 전달한 일부 필드만 변경하므로 간단한 제목·시간 변경에는 patch가 편리합니다.


11. 일정 삭제 기능

검색한 이벤트의 id를 사용해 삭제합니다.

def delete_event(
    service,
    event_id: str,
) -> None:
    (
        service.events()
        .delete(
            calendarId="primary",
            eventId=event_id,
        )
        .execute()
    )

삭제 API는 캘린더 ID와 이벤트 ID를 지정하며, 성공 시 별도 본문 없이 완료됩니다.


12. 왜 이벤트 ID가 필요한가?

내일 회의 삭제해줘

내일 일정에 회의가 여러 개 있을 수 있습니다.

10:00 팀 회의
15:00 프로젝트 회의
17:00 고객 회의

제목만으로 임의 삭제하면 잘못된 일정이 삭제될 위험이 있습니다.

필요한 절차

1. 사용자가 말한 날짜 범위 조회
2. 제목과 일치하는 일정 검색
3. 검색된 일정의 eventId 확인
4. 하나면 사용자에게 확인
5. 여러 개면 선택 목록 표시
6. 사용자가 선택한 이벤트만 삭제

13. 여러 일정이 검색될 때 처리

검색 결과가 여러 건이면 바로 삭제하지 않고 선택을 받습니다.

'회의'가 포함된 일정이 3개 있습니다.
1. 10:00 팀 회의
2. 15:00 프로젝트 회의
3. 17:00 고객 회의
삭제할 일정을 선택하세요.

텔레그램 인라인 버튼으로 선택 목록 구성

from telegram import InlineKeyboardButton
from telegram import InlineKeyboardMarkup


def make_event_keyboard(
    events: list[dict],
    action: str,
) -> InlineKeyboardMarkup:
    buttons = []

    for event in events:
        event_id = event["id"]
        title = event.get("summary", "제목 없음")

        start_data = event.get("start", {})
        start_text = start_data.get(
            "dateTime",
            start_data.get("date", ""),
        )

        buttons.append(
            [
                InlineKeyboardButton(
                    text=f"{start_text} {title}",
                    callback_data=(
                        f"{action}:{event_id}"
                    ),
                )
            ]
        )

    buttons.append(
        [
            InlineKeyboardButton(
                text="취소",
                callback_data="cancel",
            )
        ]
    )

    return InlineKeyboardMarkup(buttons)

화면에는 제목이 보이지만, 내부적으로는 정확한 eventId가 전달됩니다.


14. 실행 전 확인 기능

등록은 복구가 쉽지만, 수정·삭제는 잘못 실행하면 문제가 되므로 확인 후 실행하도록 설계합니다.

등록 확인

다음 일정을 등록할까요?
제목: 프로젝트 회의
시간: 2026-07-26 15:00~16:00
[등록] [취소]

수정 확인

다음과 같이 수정할까요?
변경 전: 2026-07-26 15:00 프로젝트 회의
변경 후: 2026-07-26 16:00 프로젝트 회의
[수정] [취소]

삭제 확인

다음 일정을 삭제할까요?
2026-07-26 16:00 프로젝트 회의
[삭제] [취소]

15. 대기 중인 작업 저장

확인 버튼을 누르기 전까지 어떤 작업인지 context.user_data에 저장해 기억합니다.

context.user_data["pending_action"] = {
    "action": "delete",
    "event_id": event["id"],
    "title": event["summary"],
}

버튼을 누르면 저장한 정보를 확인합니다.

pending = context.user_data.get(
    "pending_action"
)

if not pending:
    await query.edit_message_text(
        "처리할 작업이 없습니다."
    )
    return

실행 후에는 반드시 삭제합니다.

context.user_data.pop(
    "pending_action",
    None,
)

16. 메시지 처리 중심 함수

의도별로 처리 함수를 분리합니다.

async def handle_message(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if update.message is None:
        return

    if update.message.text is None:
        return

    text = update.message.text.strip()

    intent = detect_intent(text)

    if intent == Intent.CREATE:
        await handle_create(
            update,
            context,
            text,
        )
        return

    if intent == Intent.READ:
        await handle_read(
            update,
            context,
            text,
        )
        return

    if intent == Intent.UPDATE:
        await handle_update(
            update,
            context,
            text,
        )
        return

    if intent == Intent.DELETE:
        await handle_delete(
            update,
            context,
            text,
        )
        return

    await update.message.reply_text(
        "요청을 이해하지 못했습니다.\n\n"
        "예시\n"
        "• 내일 오후 3시 회의 등록\n"
        "• 내일 일정 알려줘\n"
        "• 내일 회의를 4시로 변경\n"
        "• 내일 회의 삭제"
    )

17. 권장 프로젝트 구조

기능이 많아지면 bot.py 하나에 모든 코드를 넣지 않는 것이 좋습니다.

telegram_calendar_bot
 ├─ .venv
 ├─ .env
 ├─ .gitignore
 ├─ bot.py
 ├─ config.py
 ├─ calendar_service.py
 ├─ command_parser.py
 ├─ handlers.py
 ├─ credentials.json
 ├─ token.json
 └─ requirements.txt

파일 역할

bot.py 봇 생성, Handler 등록, 프로그램 시작
config.py 토큰, 시간대, 기본 일정 시간 설정
calendar_service.py Google Calendar 조회·등록·수정·삭제
command_parser.py 자연어 명령 분석
handlers.py 텔레그램 메시지 및 버튼 처리
credentials.json Google OAuth 앱 인증정보
token.json Google 사용자 인증 결과

18. 업그레이드 단계

한 번에 모든 기능을 구현하지 않고 순서대로 확장하는 것이 좋습니다.

단계 내용

1단계 등록 기능 — 내일 오후 3시 회의 → 일정 등록
2단계 조회 기능 — 내일 일정 알려줘 → 일정 목록 표시
3단계 삭제 기능 — 검색 → 사용자 확인 → 삭제
4단계 수정 기능 — 기존 일정 검색 → 변경 내용 확인 → 수정
5단계 키보드 음성 입력 — 음성→텍스트 변환 후 기존 텍스트 처리 재사용
6단계 실제 음성 메시지 처리 — 파일 다운로드 → Speech-to-Text → 명령 분석

19. 테스트 시나리오

시나리오 입력 기대 결과

정상 등록 내일 오후 3시 프로젝트 회의 내일 15:00~16:00 일정 등록
일정 조회 내일 일정 알려줘 내일 일정이 시간순으로 출력
정상 수정 내일 오후 3시 프로젝트 회의를 오후 4시로 변경해줘 15:00 일정이 16:00으로 변경
정상 삭제 내일 오후 4시 프로젝트 회의 삭제해줘 확인 버튼을 누른 후 일정 삭제
여러 일정 검색 내일 회의 삭제해줘 일치하는 회의 목록을 보여주고 선택 요청
일정 없음 내일 오전 5시 회의 삭제해줘 조건에 맞는 일정을 찾지 못했습니다
모호한 시간 내일 3시 회의 등록해줘 오전/오후 확인 요청

20. 보안상 추가해야 할 기능

개인 일정이 연결되므로 다른 사람이 봇을 사용하지 못하게 해야 합니다.

.env에 허용 사용자 ID 저장

TELEGRAM_BOT_TOKEN=실제_봇_토큰
TELEGRAM_ALLOWED_USER_ID=본인_사용자_ID

권한 확인 함수

def is_authorized(
    update: Update,
) -> bool:
    if update.effective_user is None:
        return False

    return (
        update.effective_user.id
        == ALLOWED_USER_ID
    )

모든 변경 작업 전 검사

if not is_authorized(update):
    await update.message.reply_text(
        "이 봇을 사용할 권한이 없습니다."
    )
    return

공개하면 안 되는 파일

.env
credentials.json
token.json

.gitignore

.env
.venv/
credentials.json
token.json
__pycache__/
temp_voice.ogg

21. 업그레이드 버전의 핵심

기존 버전 업그레이드 버전

처리 방식 텍스트 입력 → 무조건 일정 등록 텍스트/음성 입력 → 의도 구분 → 정보 분석 → 기존 일정 검색 → 사용자 확인 → API 실행 → 결과 응답

핵심 세 가지

사용자의 명령 의도를 정확히 구분하는 것
대상 일정을 eventId로 정확히 식별하는 것
수정·삭제 전에 사용자 확인을 받는 것

정리

텔레그램 Google Calendar 봇의 업그레이드 버전은 단순한 일정 등록 프로그램을 넘어, 자연어와 음성 입력으로 일정을 조회·수정·삭제할 수 있는 개인 일정관리 도우미입니다. 특히 수정·삭제에서는 검색 결과를 확인하고 정확한 이벤트 ID를 선택한 뒤 사용자 승인을 받아 실행하는 구조가 핵심입니다.

텔레그램_구글캘린더_업그레이드_교육자료.md.pdf
0.62MB