9.0 KiB
9.0 KiB
캘린더 일정 등록 실패 문제 TDD 접근
작성일: 2025-11-22 작성자: Claude 목적: "11월 24일 월요일 오후 1시부터 4시까지 컴퍼니엑스 세미나" 등록 실패 문제 해결
1. 문제 상황
사용자 시나리오
사용자: "11월 24일 월요일 오후 1시부터 4시까지 컴퍼니엑스 세미나"
로빙: "네, 11월 24일 월요일 오후 1시부터 4시까지 컴퍼니엑스 세미나 일정을 등록해 드릴게요. 구글 캘린더에 등록해드릴까요?"
사용자: "ㅇㅇ"
로빙: "일정 등록에 실패했습니다. 날짜/시간 형식을 다시 확인해 주세요." ❌
로그 분석 (2025-11-22 11:23)
rb8001 로그:
[Calendar] Extracted - date: 2025-11-24, time_range: 13:00 ~ 16:00, location: None, title: 컴퍼니엑스 세미나
✅ 파싱 성공: 날짜, 시간, 제목 모두 정상 추출
skill-calendar 로그:
Failed to create event: ('invalid_grant: Token has been expired or revoked.', {'error': 'invalid_grant', 'error_description': 'Token has been expired or revoked.'})
❌ 문제: Google OAuth 토큰 만료/취소
2. 문제 원인 분석
2.1 rb8001 파싱 로직 (✅ 정상)
- 자연어 시간 범위 파싱: "오후 1시부터 4시까지" → "13:00 ~ 16:00" ✅
- 제목 추출: "컴퍼니엑스 세미나" ✅
- 날짜 추출: "11월 24일" → "2025-11-24" ✅
2.2 skill-calendar OAuth 토큰 문제 (❌ 실패)
- Google OAuth 토큰이 만료되었거나 취소됨
- 토큰 갱신 로직이 있지만 실패 (
invalid_grant에러) - skill-email과 동일한 패턴이지만 skill-calendar에는 제대로 적용되지 않음
3. TDD 계획
3.1 테스트 작성 (Red Phase)
테스트 1: 자연어 시간 범위 파싱
def test_natural_language_time_range_parsing():
test_cases = [
("오후 1시부터 4시까지", "13:00 ~ 16:00"),
("오전 9시부터 오후 5시까지", "09:00 ~ 17:00"),
("1시부터 4시까지", "01:00 ~ 04:00"),
]
# 검증: extract_time_range_generic() 함수
테스트 2: 제목 추출
def test_title_extraction_from_original_message():
test_cases = [
("11월 24일 월요일 오후 1시부터 4시까지 컴퍼니엑스 세미나", "컴퍼니엑스 세미나"),
]
# 검증: 원본 메시지에서 날짜/시간 제거 후 제목 추출
테스트 3: 전체 파싱 워크플로우
def test_full_parsing_workflow():
# 실제 사용자 입력 시나리오 전체 테스트
# 날짜 추출 → 시간 범위 추출 → ISO 변환
테스트 4: skill-calendar 토큰 갱신
def test_token_refresh_on_expired():
# 만료된 토큰에 대해 자동 갱신 시도
# skill-email 패턴 참고
3.2 구현 (Green Phase)
단계 1: rb8001 파싱 로직 개선 (✅ 완료)
extract_time_range_generic(): 자연어 시간 범위 파싱 추가- 제목 추출 로직 개선: 원본 메시지에서 직접 추출
calendar_approval이후calendar_confirm찾기 로직 개선
단계 2: skill-calendar 토큰 갱신 로직 확인
skill-email/services/gmail_service.py패턴 참고skill-calendar/services/google_calendar_service.py에 동일한 로직 적용- 토큰 갱신 실패 시 사용자에게 재인증 안내
3.3 리팩터링 (Refactor Phase)
- 중복 코드 제거
- 에러 처리 개선
- 로깅 개선
4. 기존 코드 확인
4.1 잘 되던 것 확인
skill-email 토큰 갱신 패턴 (참고)
# skill_email/services/gmail_service.py:64-75
if creds and hasattr(creds, 'expired') and creds.expired and creds.refresh_token:
logger.info(f"Refreshing expired token for user: {user_id}")
from google.auth.transport.requests import Request
creds.refresh(Request())
# 갱신된 토큰 저장
if hasattr(self.creds_provider, 'save_credentials'):
self.creds_provider.save_credentials(user_id, creds)
skill-calendar 현재 상태
get_credentials(): 토큰 로드만 수행create_event(): 토큰 갱신 없이 바로 API 호출- ❌ 문제: 만료된 토큰으로 API 호출 시
invalid_grant에러
4.2 중복/충돌 확인
중복 코드
skill-email과skill-calendar의 토큰 갱신 로직이 중복될 수 있음- 공통 모듈로 분리 고려 (선택사항)
충돌 없음
- rb8001 파싱 로직과 skill-calendar API는 독립적
- 문제는 skill-calendar의 OAuth 토큰 관리만 해당
5. 해결 방안
5.1 즉시 해결 (우선순위 1)
skill-calendar 토큰 갱신 로직 추가
# skill-calendar/services/google_calendar_service.py
async def get_credentials(self, user_id: str) -> Optional[Credentials]:
creds = await self._load_credentials_from_db(user_id)
if not creds:
return None
# 토큰 갱신 (skill-email 패턴 참고)
try:
if creds and hasattr(creds, 'expired') and creds.expired and creds.refresh_token:
logger.info(f"Refreshing expired token for user {user_id}")
from google.auth.transport.requests import Request
creds.refresh(Request())
# 갱신된 토큰 저장 (DB 업데이트)
await self._save_credentials_to_db(user_id, creds)
logger.info(f"Refreshed token saved for user {user_id}")
except Exception as e:
logger.warning(f"Token refresh failed for user {user_id}: {e}")
# Refresh 실패해도 계속 진행 (401 에러가 나면 그때 처리)
return creds
5.2 사용자 안내 개선 (우선순위 2)
에러 메시지 개선
# rb8001/app/router/calendar_handler.py
if not result:
logger.error(f"[Calendar] Event creation failed - OAuth token may be expired")
return {
"success": False,
"message": "일정 등록에 실패했습니다. Google 캘린더 연동을 다시 확인해 주세요."
}
5.3 장기 개선 (우선순위 3)
OAuth 재인증 플로우
- 토큰 갱신 실패 시 사용자에게 재인증 안내
- auth-server의 OAuth 재인증 URL 생성 API 활용
6. 테스트 실행 계획
6.1 단위 테스트
cd /home/admin/ivada_project/rb8001
docker exec -it rb8001 python3 tests/test_calendar_natural_language_parsing.py
6.2 통합 테스트
# 1. rb8001에서 일정 등록 요청
# 2. skill-calendar API 호출 확인
# 3. Google Calendar API 호출 확인
6.3 E2E 테스트
# 실제 사용자 시나리오 테스트
# "11월 24일 월요일 오후 1시부터 4시까지 컴퍼니엑스 세미나" 입력
# "ㅇㅇ" 승인
# Google Calendar에 일정 등록 확인
7. 참고 문서
DOCS/journey/plans/251114_skill_calendar_multiplatform_integration.md: 캘린더 통합 계획DOCS/journey/troubleshooting/250828_gmail_token_auto_refresh_RESOLVED.md: Gmail 토큰 갱신 해결 사례skill_email/services/gmail_service.py: 토큰 갱신 패턴 참고rb8001/tests/test_calendar_query_vs_create.py: 기존 캘린더 테스트
8. 해결 완료 (2025-11-22)
8.1 구현 완료
- ✅ rb8001 자연어 시간 범위 파싱 개선 (
extract_time_range_generic) - ✅ rb8001 제목 추출 로직 개선 (원본 메시지에서 직접 추출)
- ✅ skill-calendar 토큰 자동 갱신 로직 추가 (
get_credentials()) - ✅ skill-calendar
_save_credentials_to_db()메서드 구현 - ✅ skill-calendar
invalid_grant에러를 401로 전파 - ✅ rb8001 401 에러 감지 및 재인증 안내 메시지
8.2 검증 완료 (2025-11-22 11:52)
사용자: "11월 24일 월요일 오후 1시부터 4시까지 컴퍼니엑스 세미나"
로빙: "네, 다음 주 월요일 11월 24일 오후 1시부터 4시까지 컴퍼니엑스 세미나 일정을 등록해 드릴게요. 구글 캘린더에 등록해드릴까요?"
사용자: "ㅇㅇ"
로빙: "✅ 구글 캘린더에 일정을 등록했습니다!
제목: 컴퍼니엑스 세미나
일시: 2025-11-24 13:00 ~ 16:00
장소: N/A
캘린더에서 보기"
✅ 성공: 일정 등록 완료
8.3 수정된 파일
rb8001/app/router/calendar_handler.py: 자연어 시간 파싱, 제목 추출, 재인증 안내rb8001/app/skills/calendar_skill.py: 401 에러 감지 및 전파skill-calendar/services/google_calendar_service.py: 토큰 자동 갱신, DB 저장skill-calendar/routers/calendar.py:invalid_grant→ 401 변환
8.4 교훈
- 자연어 파싱: "오후 1시부터 4시까지" 같은 자연어 시간 표현을 정규식으로 파싱 가능
- 제목 추출: LLM 응답이 구조화되지 않아도 원본 메시지에서 직접 추출 가능
- OAuth 토큰 관리: skill-email 패턴을 참고하여 skill-calendar에도 동일한 토큰 갱신 로직 적용
- 에러 전파:
invalid_grant같은 인증 에러는 401로 명확히 전파하여 상위 레이어에서 처리 가능 - 사용자 안내: 기술적 에러를 사용자 친화적인 재인증 안내 메시지로 변환