# rb8001 캘린더 중복 체크 로직 수정 ## 작성일: 2025-11-17 ## 작성자: admin ## 관련 서비스: rb8001, skill-calendar, Google Calendar API --- ## 문제 상황 ### 발견된 이슈 사용자가 구글 캘린더 일정 등록을 시도할 때 다음과 같은 문제가 발생: 1. **거짓 중복 경고** - 사용자: "11월 25일 검진 인천 남동구..." - RoBeing: "일정을 구글 캘린더에 등록해드릴까요?" - 사용자: "ㅇㅇ" - RoBeing: "이미 같은 시간대에 등록된 일정이 있습니다" - **실제로는 구글 캘린더에 일정이 없음** 2. **중복 응답 문제** - 같은 메시지가 두 번씩 전송됨 - 모순된 응답 ("일정이 있다" → "일정이 없다") 3. **대화 이중 저장** - PostgreSQL에 같은 대화가 두 번 저장됨 - 각각 다른 intent로 기록 (`calendar_event`, `calendar_confirm`) --- ## 원인 분석 ### 1. 로컬 로그 기반 중복 체크의 문제 ```python # calendar_handler.py:100-119 (수정 전) # 로컬 로그 기반 중복 체크 existing = find_matching_event(user_id, target_date, target_minutes) if existing: # 로컬 DB에만 기록되어 있어도 "중복"으로 판단 duplicate_events.append(...) continue ``` **문제점:** - 로컬 DB(`calendar_event_log` 테이블)만 확인 - 실제 구글 캘린더는 조회하지 않음 - 로컬 로그와 실제 캘린더가 불일치할 수 있음 - 일정 등록 실패 후에도 로컬 로그에는 기록 - 사용자가 구글 캘린더에서 직접 삭제한 경우 - 대화 이중 저장으로 인한 로컬 로그 중복 ### 2. 대화 이중 저장 문제 데이터베이스 조회 결과: ``` ID 메시지 Intent 1777 11월 25일 검진... calendar_event 1776 11월 25일 검진... calendar_confirm ← 중복! 1782 25일 일정 등록해줘 calendar_event 1781 25일 일정 등록해줘 calendar_confirm ← 중복! ``` **원인:** - `main.py:109-115`와 `router.py:582-593`에서 이중 저장 - 각각 다른 intent로 저장되어 컨텍스트 혼란 발생 ### 3. 커밋 히스토리 - 중복 방지 기능: `ffc7406` (2025-11-17 00:27) - Claude가 추가한 미문서화 기능 - 성능 최적화 목적으로 로컬 캐시 우선 확인 - 하지만 로컬 로그 신뢰성 문제 미고려 --- ## 해결 방안 ### 1. 실제 구글 캘린더 조회로 변경 ```python # calendar_handler.py:100-119 (수정 후) # 실제 구글 캘린더 조회로 중복 체크 (로컬 로그는 신뢰할 수 없음) try: start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00")) target_date = start_dt.date() # 구글 캘린더에서 해당 날짜의 모든 일정 조회 day_start = f"{target_date.isoformat()}T00:00:00+09:00" day_end = f"{target_date.isoformat()}T23:59:59+09:00" remote_events = await calendar_skill.get_events(user_id, day_start, day_end) # 같은 시작 시간의 일정이 있는지 확인 existing = None for ev in remote_events: ev_start = ev.get("start", {}).get("dateTime") if ev_start and ev_start == start_time: existing = {"title": ev.get("summary", title), "event_id": ev.get("id")} break except Exception as e: logger.warning(f"Failed to check Google Calendar for duplicates on {d}: {e}") existing = None if existing: duplicate_events.append(...) continue ``` **개선점:** - ✅ 실제 구글 캘린더 API 호출 - ✅ 진짜 중복만 감지 - ✅ 로컬 로그 불일치 문제 해결 ### 2. 성능 고려사항 **우려:** 매번 API 호출 시 지연/비용 증가 **대응:** - 중복 체크는 일정 생성 시에만 발생 (빈도 낮음) - 사용자 경험 > 성능 최적화 (거짓 중복 방지가 우선) - 필요시 Redis 캐싱 추가 고려 --- ## 하루종일 일정 등록 지원 (2025-11-18 추가) ### 문제 상황 - "12월 25일 크리스마스 일정 등록해줘" → "시간은 하루종일" → "ㅇㅇ" → 일정 등록 실패 - "시간은 하루 종일"이 UNKNOWN으로 분류되어 `calendar_confirm` intent가 없음 - `confirm_conv`를 찾을 때 "12월 25일 크리스마스 일정 등록해줘" 메시지만 선택되어 하루종일 정보 누락 - `skill-calendar` 서비스에 `is_all_day` 파라미터가 없어서 Google Calendar API에 `dateTime` 필드를 보내 400 Bad Request 발생 ### 원인 분석 1. **의도 분류 실패**: `calendar_handler.py:42-48`에서 `calendar_confirm` intent만 찾아서 실패 시 `recent[0]` 사용, 하루종일 정보가 없는 메시지 선택 2. **하루종일 정보 수집 실패**: `calendar_handler.py:70-88`에서 `confirm_conv`의 `llm_response`에만 의존, 최근 대화에서 하루종일 정보를 별도로 수집하지 않음 3. **Google Calendar API 형식 오류**: `skill-calendar/services/google_calendar_service.py:162-172`에서 하루종일일 때도 `dateTime` 필드 사용, 실제로는 `date` 필드 필요 ### 해결 방안 1. **confirm_conv 찾기 로직 개선** (`calendar_handler.py:42-69`) - 우선순위: 1) `calendar_confirm` intent, 2) "하루 종일" 포함 메시지, 3) 최신 대화 - 최근 대화에서 "하루 종일" 포함 메시지를 별도로 추적하여 `all_day_conv`로 저장 2. **하루종일 정보 수집 강화** (`calendar_handler.py:70-88`) - `confirm_conv`의 `llm_response`에 하루종일이 없으면 최근 5개 대화에서 별도 수집 - `all_day_info` 변수로 저장하여 `time_range` 추출 시 활용 3. **슬롯 추출 패턴 확장** (`router.py:348-360`) - 시간 패턴에 "하루종일", "종일", "all day" 추가 - 하루종일 표현을 "하루종일"로 정규화하여 슬롯에 저장 4. **시간 파싱 로직 개선** (`calendar_handler.py:591-629`) - `parse_time_range` 함수에 하루종일 처리 로직 추가 - all-day 이벤트는 `date` 필드 사용 (예: "2025-12-25" → "2025-12-26") - 반환값에 `is_all_day` 플래그 추가 5. **skill-calendar 서비스 지원 추가** (`skill-calendar/routers/calendar.py:16-25`, `skill-calendar/services/google_calendar_service.py:115-186`) - `CreateEventRequest`에 `is_all_day` 필드 추가 - `GoogleCalendarService.create_event`에 `is_all_day` 파라미터 추가 - 하루종일일 때 `date` 필드 사용 (일반 이벤트는 `dateTime` 필드) 6. **중복 체크 로직 개선** (`calendar_handler.py:191-196`) - all-day 이벤트는 `date` 필드로 비교 - 일반 이벤트는 `dateTime` 필드로 비교 ### 구현 완료 - `85eee34` (2025-11-18): confirm_conv 찾기 로직 개선 - 하루종일 메시지 우선 사용 - `7bb3524` (2025-11-18): 최근 대화에서 하루종일 정보 수집 로직 추가 - `9df9e9e` (2025-11-18): generator expression에서 re 모듈 스코프 문제 해결 - `9d150c3` (2025-11-18): 함수 내부 중복 import re 제거 - `2554576` (2025-11-18): skill-calendar 서비스에 하루종일 이벤트 지원 추가 - `d1b546a` (2025-11-18): skill-calendar 인덴테이션 오류 수정 - 배포: Gitea Actions 자동 배포 완료, rb8001 및 skill-calendar 컨테이너 재시작 확인 ### 검증 - "12월 25일 크리스마스 일정 등록해줘" → "시간은 하루종일" → "ㅇㅇ" → 일정 등록 성공 - Google Calendar에서 all-day 이벤트로 정상 표시 확인 - 로그 확인: `[Calendar] Found all-day message in recent conversation`, `[Calendar] Using all_day_info from recent conversations`, `Created event rd6u0uhuqhdcvse3adovmjfkac` --- ## 대화 이중 저장 문제 (별도 해결 필요) ### 현재 상황 - `main.py`에서 저장: execution_plan의 intent 사용 - `router.py`에서 저장: task_type을 intent로 사용 - **결과:** 같은 대화가 다른 intent로 두 번 저장 ### 해결 방안 (미적용) `router.py:582`의 대화 저장을 제거하거나 조건부 실행: ```python # 제안: frontend 채널은 main.py에서만 저장 if result.get("success") and result.get("content") and channel != "frontend": await self._save_conversation(...) ``` **현재 미적용 이유:** - Slack 채널 등 다른 채널 영향도 확인 필요 - 별도 이슈로 추적 권장 --- ## 테스트 시나리오 ### 1. 정상 등록 - 사용자: "11월 25일 오전 9시 회의" - 구글 캘린더 확인: 일정 없음 - 예상: 일정 등록 성공 ### 2. 중복 감지 - 사용자: "11월 25일 오전 9시 회의" (같은 일정 재등록) - 구글 캘린더 확인: 일정 있음 - 예상: "이미 같은 시간대에 등록된 일정이 있습니다" 메시지 ### 3. 로컬 로그 불일치 - 로컬 DB에 기록 있지만 구글 캘린더에는 없는 경우 - 예상: 정상 등록 (로컬 로그 무시) --- ## 모니터링 ### 로그 확인 ```bash # 중복 체크 로그 docker logs rb8001 | grep "duplicate check" # Google Calendar API 호출 로그 docker logs rb8001 | grep "get_events" ``` ### 성능 모니터링 - 일정 생성 응답 시간 추적 - Google Calendar API 호출 빈도/실패율 - 필요시 캐싱 레이어 추가 --- ## 교훈 ### 1. 로컬 캐시의 한계 - **문제**: 성능 최적화를 위한 로컬 캐시가 오히려 오류 발생 - **교훈**: 캐시는 신뢰할 수 있을 때만 사용. Source of Truth는 실제 데이터 - **적용**: 중복 체크는 반드시 실제 구글 캘린더 조회 ### 2. 미문서화 기능의 위험 - **문제**: Claude가 추가한 기능이 문서화되지 않음 - **결과**: 의도치 않은 동작, 디버깅 어려움 - **교훈**: 모든 기능은 즉시 문서화 필수 ### 3. 이중 저장 패턴 - **의도**: ChromaDB + PostgreSQL 이중 저장은 정상 (각각 다른 목적) - **문제**: 같은 DB에 같은 데이터 중복 저장은 버그 - **교훈**: 저장 로직은 단일 진입점 유지 ### 4. UNKNOWN으로 분류된 메시지의 컨텍스트 보존 - **문제**: "시간은 하루 종일"이 UNKNOWN으로 분류되어 `calendar_confirm` intent가 없었고, `confirm_conv`를 찾을 때 하루종일 정보가 없는 메시지만 선택됨 - **교훈**: 의도 분류가 실패해도 최근 대화에서 관련 정보(하루종일, 날짜 등)를 별도로 수집하여 보존해야 함. `confirm_conv`를 찾을 때 intent뿐만 아니라 메시지 내용도 고려해야 함 ### 5. 외부 API 스펙 확인의 중요성 - **문제**: Google Calendar API의 하루종일 이벤트 형식(`date` 필드 vs `dateTime` 필드)을 확인하지 않고 구현 - **교훈**: 외부 API 통합 시 공식 문서를 먼저 확인하고 웹 검색으로 검증해야 함. 추측으로 구현하면 400 Bad Request 같은 에러 발생 --- ## 관련 파일 - `/home/admin/ivada_project/rb8001/app/router/calendar_handler.py` (수정됨) - `/home/admin/ivada_project/rb8001/app/skills/calendar_skill.py` - `/home/admin/ivada_project/rb8001/app/state/calendar_event_repository.py` ## 관련 문서 - [Gmail Calendar Scope 가이드](/home/admin/ivada_project/DOCS/journey/troubleshooting/251114_gmail_calendar_scope_reconnect_guide.md) - [rb8001 이중 저장 구현](/home/admin/ivada_project/DOCS/journey/troubleshooting/250826_happybell80_rb8001_이중저장구현.md) --- **문서 끝**