콘텐츠 대표 이미지 - 파이썬으로 AI 기반 스케줄 리마인더 만들기: 자동화부터 자연어 처리까지
🤖 AI/머신러닝 개발 · 프로그램개발

파이썬으로 AI 기반 스케줄 리마인더 만들기: 자동화부터 자연어 처리까지

📅 잊어버리는 약속, 이제 AI가 챙겨줌ㅋㅋ | 파이썬 + NLP + 스케줄링 자동화 실전 가이드

AI 스케줄 리마인더 동작 흐름 사용자 입력 "내일 오전 9시 회의" NLP 파싱 날짜·시간 추출 dateparser / spaCy 스케줄 저장 SQLite / JSON 구조화 데이터 저장 AI 알림 발송 이메일 / 슬랙 / 앱 스마트 타이밍 ✅ 완료! 리마인드 성공 핵심 기술 스택 🐍 Python 3.10+ 🧠 dateparser 📦 APScheduler 🗄️ SQLite 📧 smtplib 🤖 OpenAI API 자연어로 말하면 → AI가 알아서 파싱하고 → 딱 맞는 타이밍에 알림 쏴줌 ㄷㄷ 파이썬 하나로 이 모든 게 가능하다는 게 레전드임
🤔 왜 AI 기반 스케줄 리마인더인가?

솔직히 말해서 구글 캘린더나 애플 리마인더 이미 있잖아요? 근데 왜 굳이 파이썬으로 만드냐고ㅋㅋㅋ
이유가 있음. 기존 앱들은 정해진 포맷으로만 입력받아요. "2025년 7월 15일 오전 10시"처럼 딱딱하게.
근데 우리가 실제로 말하는 방식은 달라요. "다음 주 월요일 점심 전에 보고서 제출", "3일 후 친구 생일" 이런 식이잖아요?

AI 기반 리마인더는 이런 자연어(Natural Language)를 그대로 이해하고 처리해줘요.
거기다 파이썬으로 직접 만들면 내 워크플로우에 딱 맞게 커스터마이징이 가능하고, 슬랙·이메일·텔레그램 등 원하는 채널로 알림을 보낼 수 있어요.
이게 진짜 개발자 감성 아닌가요ㅋㅋ 내 손으로 만든 AI 비서, 생각만 해도 설레지 않음?

💡 이 글에서 만들 것: 자연어 입력 → NLP 파싱 → SQLite 저장 → APScheduler 스케줄링 → 이메일/슬랙 알림 발송까지 이어지는 풀스택 AI 리마인더 시스템

🛠️ 환경 세팅: 필요한 라이브러리 설치

먼저 필요한 패키지들 설치부터 해봐요. 터미널 열고 아래 명령어 복붙ㄱㄱ

pip install apscheduler dateparser openai python-dotenv
pip install slack-sdk  # 슬랙 알림 원하면
pip install pytz       # 시간대 처리
pip install spacy      # 고급 NLP 원하면
python -m spacy download ko_core_news_sm  # 한국어 모델

각 라이브러리가 뭐 하는 애들인지 정리해볼게요:

라이브러리 역할 특징
APScheduler 작업 스케줄링 크론 표현식, 인터벌, 특정 시간 등 다양한 트리거 지원
dateparser 자연어 날짜 파싱 "내일", "다음 주 월요일" 같은 표현 처리 가능
OpenAI API GPT 기반 NLP 복잡한 자연어 이해, 의도 파악
python-dotenv 환경변수 관리 API 키 등 민감 정보 .env 파일로 관리
pytz 시간대 처리 한국 시간(KST) 등 타임존 변환
💡 가상환경 쓰는 거 강추임! python -m venv reminder_env 로 가상환경 만들고 시작하면 의존성 충돌 걱정 없어요.

🗄️ 1단계: 데이터베이스 설계 (SQLite)

스케줄 데이터를 저장할 DB부터 만들어요. SQLite 쓰면 별도 서버 없이도 파일 하나로 DB 운영 가능해서 개인 프로젝트에 딱이에요ㅋㅋ

import sqlite3
from datetime import datetime

def init_database():
    """데이터베이스 초기화 및 테이블 생성"""
    conn = sqlite3.connect('reminders.db')
    cursor = conn.cursor()
    
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS reminders (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            original_text TEXT NOT NULL,       -- 사용자가 입력한 원본 텍스트
            title TEXT NOT NULL,               -- 파싱된 일정 제목
            remind_time DATETIME NOT NULL,     -- 알림 시간
            notify_before INTEGER DEFAULT 10,  -- 몇 분 전 알림 (기본 10분)
            channel TEXT DEFAULT 'email',      -- 알림 채널 (email/slack/both)
            status TEXT DEFAULT 'pending',     -- pending/sent/cancelled
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            ai_parsed_data TEXT               -- AI가 파싱한 JSON 데이터
        )
    ''')
    
    conn.commit()
    conn.close()
    print("✅ 데이터베이스 초기화 완료!")

init_database()

테이블 설계 포인트를 짚어볼게요:
original_text는 사용자가 입력한 원본 텍스트를 그대로 저장해요. 나중에 AI 재파싱이나 디버깅할 때 유용해요.
ai_parsed_data는 AI가 분석한 결과를 JSON 형태로 저장해서, 추후 분석이나 학습 데이터로 활용 가능해요.
notify_before는 몇 분 전에 알림을 줄지 설정하는 값이에요. 기본 10분이지만 사용자가 커스터마이징 가능하게 설계했어요.


🧠 2단계: AI 자연어 파싱 엔진 구축

이게 진짜 핵심이에요! 사용자가 "다음 주 화요일 오후 3시에 팀 미팅 있어"라고 입력하면, 이걸 파이썬이 이해할 수 있는 datetime 객체로 변환해야 해요.
두 가지 방법을 조합해서 쓸 거예요: dateparser (빠르고 가벼움) + OpenAI GPT (복잡한 케이스 처리)

📌 방법 1: dateparser로 기본 파싱
import dateparser
import pytz
from datetime import datetime

KST = pytz.timezone('Asia/Seoul')

def parse_datetime_basic(text: str) -> datetime | None:
    """
    dateparser를 이용한 기본 날짜/시간 파싱
    한국어 자연어 표현 지원
    """
    settings = {
        'PREFER_DATES_FROM': 'future',  # 미래 날짜 우선
        'TIMEZONE': 'Asia/Seoul',
        'RETURN_AS_TIMEZONE_AWARE': True,
        'LANGUAGES': ['ko', 'en'],      # 한국어, 영어 지원
    }
    
    parsed = dateparser.parse(text, settings=settings)
    return parsed

# 테스트
test_cases = [
    "내일 오전 9시",
    "다음 주 월요일 오후 2시",
    "3일 후 저녁 7시",
    "이번 금요일 점심",
    "2025년 8월 15일 오전 10시",
]

for case in test_cases:
    result = parse_datetime_basic(case)
    print(f"입력: {case}")
    print(f"파싱: {result}")
    print("---")
📌 방법 2: OpenAI GPT로 고급 파싱

dateparser가 못 잡는 복잡한 표현들, 예를 들어 "다음 달 첫 번째 평일", "분기 마감 3일 전" 같은 건 GPT한테 맡겨요ㅋㅋ

import openai
import json
import os
from dotenv import load_dotenv

load_dotenv()
client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))

def parse_with_gpt(user_input: str) -> dict:
    """
    GPT를 이용한 고급 자연어 파싱
    복잡한 날짜 표현, 반복 일정, 조건부 일정 처리
    """
    current_time = datetime.now(KST).strftime('%Y-%m-%d %H:%M:%S')
    
    prompt = f"""
현재 시간: {current_time} (한국 시간 KST)

사용자 입력: "{user_input}"

위 입력에서 다음 정보를 JSON 형식으로 추출해줘:
{{
    "title": "일정 제목 (핵심 내용만)",
    "datetime": "YYYY-MM-DD HH:MM:SS 형식",
    "is_recurring": false,
    "recurrence_rule": null,
    "notify_before_minutes": 10,
    "confidence": 0.95,
    "notes": "추가 메모나 불확실한 부분"
}}

규칙:
- datetime은 반드시 미래 시간이어야 함
- 시간이 명시되지 않으면 해당 날 오전 9시로 설정
- "점심"은 12:00, "저녁"은 18:00, "아침"은 08:00으로 처리
- 반복 일정이면 is_recurring을 true로 설정
- JSON만 반환, 다른 텍스트 없이
"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",  # 비용 효율적인 모델
        messages=[
            {"role": "system", "content": "당신은 한국어 일정 파싱 전문가입니다. JSON만 반환합니다."},
            {"role": "user", "content": prompt}
        ],
        temperature=0.1,  # 낮은 temperature로 일관성 확보
        response_format={"type": "json_object"}
    )
    
    result = json.loads(response.choices[0].message.content)
    return result

# 테스트
complex_cases = [
    "다음 달 첫 번째 평일 오전에 병원 예약",
    "매주 월요일 오전 10시 팀 스탠드업 미팅",
    "프로젝트 마감 이틀 전 오후 5시에 최종 검토",
]

for case in complex_cases:
    result = parse_with_gpt(case)
    print(f"입력: {case}")
    print(f"결과: {json.dumps(result, ensure_ascii=False, indent=2)}")
    print("---")
📌 하이브리드 파서: 두 방법 조합

실제 프로덕션에서는 dateparser 먼저 시도하고, 실패하거나 신뢰도가 낮으면 GPT로 폴백하는 방식이 비용 효율적이에요.

def smart_parse(user_input: str) -> dict:
    """
    하이브리드 파서: dateparser 우선, GPT 폴백
    """
    # 1차: dateparser 시도
    basic_result = parse_datetime_basic(user_input)
    
    if basic_result and basic_result > datetime.now(KST):
        # dateparser 성공 시
        return {
            "title": extract_title(user_input),  # 별도 제목 추출
            "datetime": basic_result.strftime('%Y-%m-%d %H:%M:%S'),
            "is_recurring": False,
            "notify_before_minutes": 10,
            "confidence": 0.8,
            "source": "dateparser"
        }
    else:
        # 2차: GPT 폴백
        print("🤖 GPT 파싱 모드 활성화...")
        gpt_result = parse_with_gpt(user_input)
        gpt_result["source"] = "gpt"
        return gpt_result

def extract_title(text: str) -> str:
    """간단한 제목 추출 (날짜/시간 표현 제거)"""
    import re
    # 날짜/시간 관련 패턴 제거
    patterns = [
        r'(내일|모레|오늘|다음\s*주|이번\s*주)',
        r'(오전|오후|아침|점심|저녁)\s*\d+시',
        r'\d+일\s*(후|뒤)',
        r'\d{4}년\s*\d+월\s*\d+일',
    ]
    result = text
    for pattern in patterns:
        result = re.sub(pattern, '', result)
    return result.strip() or text[:20]
⚠️ GPT API 비용 주의! gpt-4o-mini 기준으로 1000 토큰당 약 $0.00015 정도예요. 개인 사용 수준에서는 거의 무료에 가깝지만, 대량 처리 시에는 비용 모니터링 필수!

💾 3단계: 리마인더 저장 및 관리

파싱한 데이터를 DB에 저장하고 관리하는 CRUD 함수들 만들어요.

import sqlite3
import json
from datetime import datetime

def save_reminder(original_text: str, parsed_data: dict, channel: str = 'email') -> int:
    """리마인더를 DB에 저장하고 ID 반환"""
    conn = sqlite3.connect('reminders.db')
    cursor = conn.cursor()
    
    remind_datetime = datetime.strptime(
        parsed_data['datetime'], 
        '%Y-%m-%d %H:%M:%S'
    )
    
    # 알림 시간 = 일정 시간 - notify_before_minutes
    from datetime import timedelta
    notify_time = remind_datetime - timedelta(
        minutes=parsed_data.get('notify_before_minutes', 10)
    )
    
    cursor.execute('''
        INSERT INTO reminders 
        (original_text, title, remind_time, notify_before, channel, ai_parsed_data)
        VALUES (?, ?, ?, ?, ?, ?)
    ''', (
        original_text,
        parsed_data['title'],
        notify_time.strftime('%Y-%m-%d %H:%M:%S'),
        parsed_data.get('notify_before_minutes', 10),
        channel,
        json.dumps(parsed_data, ensure_ascii=False)
    ))
    
    reminder_id = cursor.lastrowid
    conn.commit()
    conn.close()
    
    print(f"✅ 리마인더 저장 완료! ID: {reminder_id}")
    print(f"   📅 일정: {parsed_data['title']}")
    print(f"   ⏰ 알림 시간: {notify_time.strftime('%Y년 %m월 %d일 %H:%M')}")
    
    return reminder_id

def get_pending_reminders() -> list:
    """발송 대기 중인 리마인더 목록 조회"""
    conn = sqlite3.connect('reminders.db')
    cursor = conn.cursor()
    
    cursor.execute('''
        SELECT id, title, remind_time, channel, notify_before
        FROM reminders
        WHERE status = 'pending'
        AND remind_time > datetime('now')
        ORDER BY remind_time ASC
    ''')
    
    reminders = cursor.fetchall()
    conn.close()
    return reminders

def mark_as_sent(reminder_id: int):
    """리마인더를 발송 완료로 표시"""
    conn = sqlite3.connect('reminders.db')
    cursor = conn.cursor()
    cursor.execute(
        "UPDATE reminders SET status = 'sent' WHERE id = ?",
        (reminder_id,)
    )
    conn.commit()
    conn.close()

def cancel_reminder(reminder_id: int):
    """리마인더 취소"""
    conn = sqlite3.connect('reminders.db')
    cursor = conn.cursor()
    cursor.execute(
        "UPDATE reminders SET status = 'cancelled' WHERE id = ?",
        (reminder_id,)
    )
    conn.commit()
    conn.close()
    print(f"🚫 리마인더 {reminder_id} 취소됨")

📧 4단계: 알림 발송 시스템 구축

이제 실제로 알림을 보내는 모듈 만들어요. 이메일이랑 슬랙 두 가지 채널 지원할 거예요.

📌 이메일 알림 (smtplib)
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import os

def send_email_reminder(reminder: dict) -> bool:
    """
    Gmail SMTP를 이용한 이메일 알림 발송
    Gmail 앱 비밀번호 필요 (2단계 인증 설정 후 발급)
    """
    sender_email = os.getenv('GMAIL_ADDRESS')
    sender_password = os.getenv('GMAIL_APP_PASSWORD')  # 앱 비밀번호!
    receiver_email = os.getenv('RECEIVER_EMAIL')
    
    # 이메일 내용 구성
    msg = MIMEMultipart('alternative')
    msg['Subject'] = f"⏰ 리마인더: {reminder['title']}"
    msg['From'] = sender_email
    msg['To'] = receiver_email
    
    # HTML 이메일 본문
    html_body = f"""
    <html>
    <body style="font-family: 'Noto Sans KR', sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
        <div style="background: linear-gradient(135deg, #e8e0f7, #d4eaf7); border-radius: 16px; padding: 24px; text-align: center;">
            <h2 style="color: #3a2d7a; margin: 0 0 8px 0;">⏰ 일정 리마인더</h2>
            <p style="color: #6b7a99; margin: 0;">AI 스케줄 리마인더가 알려드립니다</p>
        </div>
        
        <div style="background: #fff; border-radius: 12px; padding: 24px; margin-top: 16px; border: 1px solid #e8e0f7;">
            <h3 style="color: #333; margin: 0 0 12px 0;">📅 {reminder['title']}</h3>
            <p style="color: #666; margin: 0;">
                <strong>일정 시간:</strong> {reminder['remind_time']}<br>
                <strong>원본 입력:</strong> {reminder['original_text']}
            </p>
        </div>
        
        <p style="text-align: center; color: #a89de8; font-size: 12px; margin-top: 16px;">
            Python AI 스케줄 리마인더 | 자동 발송 메일
        </p>
    </body>
    </html>
    """
    
    part = MIMEText(html_body, 'html', 'utf-8')
    msg.attach(part)
    
    try:
        with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, receiver_email, msg.as_string())
        print(f"📧 이메일 발송 성공: {reminder['title']}")
        return True
    except Exception as e:
        print(f"❌ 이메일 발송 실패: {e}")
        return False
📌 슬랙 알림 (Slack SDK)
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

def send_slack_reminder(reminder: dict) -> bool:
    """
    Slack Bot을 통한 채널 알림 발송
    Slack App 생성 후 Bot Token 필요
    """
    client = WebClient(token=os.getenv('SLACK_BOT_TOKEN'))
    channel_id = os.getenv('SLACK_CHANNEL_ID')
    
    # Slack Block Kit으로 예쁜 메시지 구성
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": f"⏰ 리마인더: {reminder['title']}"
            }
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": f"*📅 일정 시간:*\n{reminder['remind_time']}"
                },
                {
                    "type": "mrkdwn",
                    "text": f"*📝 원본 입력:*\n{reminder['original_text']}"
                }
            ]
        },
        {
            "type": "context",
            "elements": [
                {
                    "type": "mrkdwn",
                    "text": "🤖 _Python AI 스케줄 리마인더 자동 발송_"
                }
            ]
        }
    ]
    
    try:
        response = client.chat_postMessage(
            channel=channel_id,
            blocks=blocks,
            text=f"리마인더: {reminder['title']}"  # 폴백 텍스트
        )
        print(f"💬 슬랙 알림 발송 성공: {reminder['title']}")
        return True
    except SlackApiError as e:
        print(f"❌ 슬랙 발송 실패: {e.response['error']}")
        return False

def send_notification(reminder: dict) -> bool:
    """채널에 따라 적절한 알림 발송"""
    channel = reminder.get('channel', 'email')
    
    if channel == 'email':
        return send_email_reminder(reminder)
    elif channel == 'slack':
        return send_slack_reminder(reminder)
    elif channel == 'both':
        email_ok = send_email_reminder(reminder)
        slack_ok = send_slack_reminder(reminder)
        return email_ok or slack_ok
    else:
        print(f"⚠️ 알 수 없는 채널: {channel}")
        return False
💡 Gmail 앱 비밀번호 발급 방법: Google 계정 → 보안 → 2단계 인증 활성화 → 앱 비밀번호 생성. 일반 비밀번호 대신 이 앱 비밀번호를 사용해야 해요!

⏱️ 5단계: APScheduler로 자동 스케줄링

이제 진짜 핵심! APScheduler가 백그라운드에서 돌면서 알림 시간이 되면 자동으로 발송해줘요.
이게 없으면 그냥 DB에 저장만 하고 끝이잖아요ㅋㅋ 이게 있어야 진짜 자동화가 되는 거예요.

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
import pytz

KST = pytz.timezone('Asia/Seoul')

# 스케줄러 설정 (영속성 있는 JobStore 사용)
jobstores = {
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.db')
}

scheduler = BackgroundScheduler(
    jobstores=jobstores,
    timezone=KST
)

def reminder_job(reminder_id: int, title: str, original_text: str, 
                 remind_time: str, channel: str):
    """스케줄러가 실행할 실제 알림 작업"""
    print(f"\n🔔 알림 실행! [{title}]")
    
    reminder = {
        'id': reminder_id,
        'title': title,
        'original_text': original_text,
        'remind_time': remind_time,
        'channel': channel
    }
    
    success = send_notification(reminder)
    
    if success:
        mark_as_sent(reminder_id)
        print(f"✅ 알림 발송 완료: {title}")
    else:
        print(f"❌ 알림 발송 실패: {title}")

def schedule_reminder(reminder_id: int, reminder_data: dict, channel: str = 'email'):
    """리마인더를 스케줄러에 등록"""
    remind_time = datetime.strptime(
        reminder_data['datetime'], 
        '%Y-%m-%d %H:%M:%S'
    )
    remind_time = KST.localize(remind_time)
    
    # notify_before_minutes 전에 알림
    from datetime import timedelta
    notify_time = remind_time - timedelta(
        minutes=reminder_data.get('notify_before_minutes', 10)
    )
    
    # 반복 일정 처리
    if reminder_data.get('is_recurring') and reminder_data.get('recurrence_rule'):
        # 크론 표현식으로 반복 스케줄 등록
        trigger = CronTrigger.from_crontab(
            reminder_data['recurrence_rule'],
            timezone=KST
        )
    else:
        # 단일 알림
        trigger = DateTrigger(run_date=notify_time, timezone=KST)
    
    job = scheduler.add_job(
        reminder_job,
        trigger=trigger,
        id=f"reminder_{reminder_id}",
        args=[
            reminder_id,
            reminder_data['title'],
            reminder_data.get('original_text', ''),
            reminder_data['datetime'],
            channel
        ],
        replace_existing=True,
        misfire_grace_time=300  # 5분 이내 지연 허용
    )
    
    print(f"📌 스케줄 등록 완료!")
    print(f"   Job ID: {job.id}")
    print(f"   알림 시간: {notify_time.strftime('%Y년 %m월 %d일 %H:%M')}")
    
    return job.id

def load_pending_reminders_to_scheduler():
    """앱 시작 시 DB의 대기 중인 리마인더를 스케줄러에 로드"""
    pending = get_pending_reminders()
    loaded_count = 0
    
    for reminder in pending:
        reminder_id, title, remind_time, channel, notify_before = reminder
        
        remind_datetime = datetime.strptime(remind_time, '%Y-%m-%d %H:%M:%S')
        remind_datetime = KST.localize(remind_datetime)
        
        if remind_datetime > datetime.now(KST):
            trigger = DateTrigger(run_date=remind_datetime, timezone=KST)
            scheduler.add_job(
                reminder_job,
                trigger=trigger,
                id=f"reminder_{reminder_id}",
                args=[reminder_id, title, '', remind_time, channel],
                replace_existing=True
            )
            loaded_count += 1
    
    print(f"🔄 {loaded_count}개의 대기 중인 리마인더 로드 완료")

🎯 6단계: 메인 애플리케이션 통합

이제 모든 모듈을 하나로 합쳐서 실제로 동작하는 앱을 만들어요!
CLI 인터페이스로 만들어서 터미널에서 바로 사용할 수 있게 할 거예요.

import time
import sys

def main():
    """메인 애플리케이션 실행"""
    print("=" * 50)
    print("🤖 AI 스케줄 리마인더 시작!")
    print("=" * 50)
    
    # 1. 데이터베이스 초기화
    init_database()
    
    # 2. 스케줄러 시작
    scheduler.start()
    print("⚡ 스케줄러 가동 중...")
    
    # 3. 기존 대기 중인 리마인더 로드
    load_pending_reminders_to_scheduler()
    
    print("\n💬 사용법:")
    print("  - 일정 추가: '내일 오전 10시 팀 미팅'")
    print("  - 목록 보기: 'list'")
    print("  - 취소: 'cancel [ID]'")
    print("  - 종료: 'quit'")
    print("-" * 50)
    
    while True:
        try:
            user_input = input("\n📝 일정 입력 > ").strip()
            
            if not user_input:
                continue
            
            if user_input.lower() == 'quit':
                print("👋 리마인더 종료!")
                scheduler.shutdown()
                break
            
            elif user_input.lower() == 'list':
                show_reminder_list()
            
            elif user_input.lower().startswith('cancel '):
                reminder_id = int(user_input.split()[1])
                cancel_reminder(reminder_id)
                scheduler.remove_job(f"reminder_{reminder_id}")
            
            else:
                # 일정 추가 처리
                print("🔍 AI 파싱 중...")
                parsed = smart_parse(user_input)
                
                if parsed and parsed.get('datetime'):
                    # 채널 선택
                    channel = input("📡 알림 채널 (email/slack/both) [기본: email]: ").strip()
                    if channel not in ['email', 'slack', 'both']:
                        channel = 'email'
                    
                    # DB 저장
                    parsed['original_text'] = user_input
                    reminder_id = save_reminder(user_input, parsed, channel)
                    
                    # 스케줄러 등록
                    schedule_reminder(reminder_id, parsed, channel)
                    
                    print(f"\n✨ 완료! '{parsed['title']}' 리마인더가 등록됐어요!")
                else:
                    print("❌ 날짜/시간을 파싱하지 못했어요. 더 명확하게 입력해줘요!")
        
        except KeyboardInterrupt:
            print("\n\n👋 종료!")
            scheduler.shutdown()
            break
        except Exception as e:
            print(f"❌ 오류 발생: {e}")

def show_reminder_list():
    """등록된 리마인더 목록 출력"""
    reminders = get_pending_reminders()
    
    if not reminders:
        print("📭 등록된 리마인더가 없어요!")
        return
    
    print("\n📋 등록된 리마인더 목록:")
    print("-" * 60)
    for r in reminders:
        reminder_id, title, remind_time, channel, notify_before = r
        print(f"  [{reminder_id}] {title}")
        print(f"       ⏰ {remind_time} | 📡 {channel} | 🔔 {notify_before}분 전")
    print("-" * 60)

if __name__ == "__main__":
    main()
💡 실행 방법: python reminder.py 로 실행하면 바로 사용 가능! 백그라운드에서 계속 돌리려면 nohup python reminder.py & 또는 systemd 서비스로 등록하면 돼요.

🚀 7단계: 고급 기능 추가하기
📌 스마트 알림 타이밍 (AI 최적화)

단순히 "10분 전"이 아니라, 일정의 중요도와 유형에 따라 AI가 최적의 알림 타이밍을 결정하게 할 수 있어요.

def get_smart_notify_time(title: str, event_type: str = None) -> int:
    """
    일정 유형에 따른 스마트 알림 시간 결정
    반환값: 몇 분 전에 알림을 줄지
    """
    # 키워드 기반 분류
    urgent_keywords = ['마감', '제출', '발표', '시험', '면접']
    travel_keywords = ['비행기', '기차', '버스', '출발', '공항']
    meeting_keywords = ['미팅', '회의', '미팅', '약속', '만남']
    
    title_lower = title.lower()
    
    if any(kw in title for kw in urgent_keywords):
        return 60  # 중요 마감: 1시간 전
    elif any(kw in title for kw in travel_keywords):
        return 120  # 이동 관련: 2시간 전
    elif any(kw in title for kw in meeting_keywords):
        return 15  # 미팅: 15분 전
    else:
        return 10  # 기본: 10분 전

# GPT 버전 (더 정교함)
def get_ai_notify_time(title: str, event_datetime: str) -> int:
    """GPT를 이용한 지능적 알림 시간 결정"""
    prompt = f"""
일정: "{title}"
일정 시간: {event_datetime}

이 일정에 대해 몇 분 전에 알림을 보내는 게 적절할지 숫자만 반환해줘.
예: 30 (30분 전을 의미)

고려사항:
- 이동이 필요한 일정: 60-120분 전
- 중요한 마감/제출: 60분 전  
- 일반 미팅/약속: 15-30분 전
- 간단한 할 일: 5-10분 전
"""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.1
    )
    
    try:
        minutes = int(response.choices[0].message.content.strip())
        return max(5, min(minutes, 1440))  # 5분 ~ 24시간 사이로 제한
    except:
        return 10  # 파싱 실패 시 기본값
📌 반복 일정 처리
def add_recurring_reminder(user_input: str, channel: str = 'email'):
    """
    반복 일정 추가
    예: "매주 월요일 오전 9시 팀 스탠드업"
    """
    parsed = parse_with_gpt(user_input)
    
    if not parsed.get('is_recurring'):
        print("⚠️ 반복 일정이 아닌 것 같아요. 일반 리마인더로 추가할게요.")
        return add_single_reminder(user_input, channel)
    
    # 반복 규칙 생성
    recurrence_map = {
        '매일': '0 9 * * *',          # 매일 오전 9시
        '매주 월요일': '0 9 * * 1',   # 매주 월요일 오전 9시
        '매주 금요일': '0 18 * * 5',  # 매주 금요일 오후 6시
        '매월 1일': '0 9 1 * *',      # 매월 1일 오전 9시
    }
    
    # GPT가 생성한 크론 표현식 사용
    cron_expr = parsed.get('recurrence_rule', '0 9 * * 1')
    
    reminder_id = save_reminder(user_input, parsed, channel)
    
    # 크론 트리거로 스케줄 등록
    trigger = CronTrigger.from_crontab(cron_expr, timezone=KST)
    scheduler.add_job(
        reminder_job,
        trigger=trigger,
        id=f"recurring_{reminder_id}",
        args=[reminder_id, parsed['title'], user_input, '', channel],
        replace_existing=True
    )
    
    print(f"🔄 반복 리마인더 등록 완료!")
    print(f"   크론 표현식: {cron_expr}")
📌 .env 파일 설정

API 키 등 민감한 정보는 절대 코드에 직접 넣으면 안 돼요! .env 파일로 관리해요.

# .env 파일 내용
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx
GMAIL_ADDRESS=your.email@gmail.com
GMAIL_APP_PASSWORD=xxxx xxxx xxxx xxxx
RECEIVER_EMAIL=receiver@gmail.com
SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxxxxxxxxxxxxxx
SLACK_CHANNEL_ID=C0XXXXXXXXX
⚠️ .env 파일은 반드시 .gitignore에 추가! GitHub에 올리면 API 키 노출돼서 큰일 나요. echo ".env" >> .gitignore 잊지 마세요!

📊 전체 프로젝트 구조 정리

지금까지 만든 것들을 깔끔하게 모듈화하면 이런 구조가 돼요:

ai_reminder/
├── main.py              # 메인 실행 파일
├── database.py          # DB 초기화 및 CRUD
├── parser/
│   ├── __init__.py
│   ├── basic_parser.py  # dateparser 기반 파서
│   └── gpt_parser.py    # OpenAI GPT 파서
├── notifier/
│   ├── __init__.py
│   ├── email_notifier.py  # 이메일 알림
│   └── slack_notifier.py  # 슬랙 알림
├── scheduler.py         # APScheduler 설정
├── .env                 # 환경변수 (gitignore!)
├── .gitignore
├── requirements.txt
└── reminders.db         # SQLite DB (자동 생성)
🗣️ 자연어 입력 → 🧠 AI 파싱 → 💾 DB 저장 → ⏱️ 스케줄 등록 → 📧 알림 발송 → ✅ 완료!

🔧 트러블슈팅 & 자주 하는 실수들
❌ 실수 1: 시간대(Timezone) 처리 안 함

파이썬에서 datetime 다룰 때 timezone 처리 안 하면 진짜 골치 아파요ㅋㅋ
특히 서버가 UTC로 돌아가는데 한국 시간으로 알림 설정하면 9시간 차이 나서 엉뚱한 시간에 알림 와요.

# ❌ 잘못된 방법
from datetime import datetime
remind_time = datetime(2025, 8, 15, 10, 0, 0)  # timezone 없음!

# ✅ 올바른 방법
import pytz
KST = pytz.timezone('Asia/Seoul')
remind_time = KST.localize(datetime(2025, 8, 15, 10, 0, 0))

# 또는 Python 3.9+
from zoneinfo import ZoneInfo
remind_time = datetime(2025, 8, 15, 10, 0, 0, tzinfo=ZoneInfo('Asia/Seoul'))
❌ 실수 2: APScheduler 종료 시 작업 손실

메모리 기반 JobStore 쓰면 앱 재시작 시 모든 스케줄이 사라져요!
SQLAlchemyJobStore 써서 영속성 확보하는 게 필수예요.

❌ 실수 3: GPT API 오류 처리 미흡
def safe_gpt_parse(user_input: str) -> dict | None:
    """안전한 GPT 파싱 (오류 처리 포함)"""
    try:
        result = parse_with_gpt(user_input)
        return result
    except openai.RateLimitError:
        print("⚠️ API 한도 초과. 잠시 후 재시도...")
        time.sleep(60)
        return parse_with_gpt(user_input)  # 1회 재시도
    except openai.APIConnectionError:
        print("⚠️ API 연결 오류. dateparser로 폴백...")
        return None  # dateparser로 폴백
    except json.JSONDecodeError:
        print("⚠️ GPT 응답 파싱 오류")
        return None
    except Exception as e:
        print(f"❌ 예상치 못한 오류: {e}")
        return None
💡 재능넷에서 파이썬 AI 개발 관련 재능을 찾거나 공유할 때, 이런 실전 프로젝트 경험이 있으면 훨씬 신뢰도가 올라가요! 직접 만든 포트폴리오는 최고의 증명이니까요 😊

📈 성능 최적화 & 확장 아이디어
기능 구현 방법 난이도
🌐 웹 UI 추가 FastAPI + React 또는 Streamlit ⭐⭐⭐
📱 텔레그램 봇 python-telegram-bot 라이브러리 ⭐⭐
🔊 음성 입력 OpenAI Whisper API ⭐⭐
📊 일정 분석 pandas + matplotlib 시각화 ⭐⭐
🔗 캘린더 연동 Google Calendar API ⭐⭐⭐
🤖 챗봇 인터페이스 LangChain + 대화형 UI ⭐⭐⭐⭐
📌 텔레그램 봇 연동 (간단 예시)
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters

async def handle_message(update: Update, context):
    """텔레그램 메시지 처리"""
    user_text = update.message.text
    
    # AI 파싱
    parsed = smart_parse(user_text)
    
    if parsed and parsed.get('datetime'):
        reminder_id = save_reminder(user_text, parsed, 'telegram')
        schedule_reminder(reminder_id, parsed, 'telegram')
        
        await update.message.reply_text(
            f"✅ 리마인더 등록 완료!\n"
            f"📅 {parsed['title']}\n"
            f"⏰ {parsed['datetime']}"
        )
    else:
        await update.message.reply_text(
            "❌ 날짜/시간을 파악하지 못했어요. 다시 입력해줘요!"
        )

# 봇 실행
app = Application.builder().token(os.getenv('TELEGRAM_BOT_TOKEN')).build()
app.add_handler(MessageHandler(filters.TEXT, handle_message))
app.run_polling()

🎓 마무리: 이 프로젝트로 배운 것들

이 프로젝트 하나로 진짜 많은 걸 배울 수 있어요. 정리해볼게요:

🧠 NLP & AI 활용 dateparser로 한국어 자연어 날짜 파싱하는 법, OpenAI API를 실제 프로덕션 코드에 통합하는 법, 하이브리드 파서 설계 패턴까지 배울 수 있어요.

⚙️ 백엔드 자동화
APScheduler로 백그라운드 작업 스케줄링, SQLite로 데이터 영속성 관리, 모듈화된 코드 구조 설계까지.

📡 외부 서비스 연동
Gmail SMTP, Slack API, 텔레그램 봇 등 실제 서비스와 연동하는 실전 경험을 쌓을 수 있어요.

🔐 보안 & 환경 관리
.env 파일로 API 키 관리, gitignore 설정, 오류 처리 패턴 등 실무에서 바로 쓸 수 있는 습관들.

이 정도 프로젝트를 완성하면 파이썬 AI 개발 포트폴리오로 충분히 활용할 수 있어요.
재능넷 같은 플랫폼에서 파이썬 자동화나 AI 개발 재능을 공유할 때도 이런 실전 프로젝트 경험이 있으면 훨씬 설득력 있어요ㅋㅋ

처음엔 복잡해 보여도 하나씩 모듈 단위로 만들다 보면 어느새 완성되어 있을 거예요.
"내일 오전 9시 팀 미팅" 한 마디에 AI가 알아서 알림 보내주는 그 순간, 진짜 개발의 재미를 느낄 수 있을 거예요 🚀

🔔 내일 오전 9시 미팅! AI가 알아서 챙겨드림 ㅋㅋ 파이썬 + AI = 나만의 스마트 비서 완성! 🚀 자연어로 말하면 AI가 알아서 파싱하고 딱 맞는 타이밍에 알림 쏴줌
댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.