docs: 캘린더 일정 등록 실패 문제 TDD 접근 문서 추가
This commit is contained in:
parent
5dc72d5408
commit
ffbdd6037a
@ -0,0 +1,231 @@
|
|||||||
|
# 캘린더 일정 등록 실패 문제 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: 자연어 시간 범위 파싱
|
||||||
|
```python
|
||||||
|
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: 제목 추출
|
||||||
|
```python
|
||||||
|
def test_title_extraction_from_original_message():
|
||||||
|
test_cases = [
|
||||||
|
("11월 24일 월요일 오후 1시부터 4시까지 컴퍼니엑스 세미나", "컴퍼니엑스 세미나"),
|
||||||
|
]
|
||||||
|
# 검증: 원본 메시지에서 날짜/시간 제거 후 제목 추출
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 테스트 3: 전체 파싱 워크플로우
|
||||||
|
```python
|
||||||
|
def test_full_parsing_workflow():
|
||||||
|
# 실제 사용자 입력 시나리오 전체 테스트
|
||||||
|
# 날짜 추출 → 시간 범위 추출 → ISO 변환
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 테스트 4: skill-calendar 토큰 갱신
|
||||||
|
```python
|
||||||
|
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 토큰 갱신 패턴 (참고)
|
||||||
|
```python
|
||||||
|
# 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 토큰 갱신 로직 추가
|
||||||
|
```python
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
#### 에러 메시지 개선
|
||||||
|
```python
|
||||||
|
# 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 단위 테스트
|
||||||
|
```bash
|
||||||
|
cd /home/admin/ivada_project/rb8001
|
||||||
|
docker exec -it rb8001 python3 tests/test_calendar_natural_language_parsing.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 통합 테스트
|
||||||
|
```bash
|
||||||
|
# 1. rb8001에서 일정 등록 요청
|
||||||
|
# 2. skill-calendar API 호출 확인
|
||||||
|
# 3. Google Calendar API 호출 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 E2E 테스트
|
||||||
|
```bash
|
||||||
|
# 실제 사용자 시나리오 테스트
|
||||||
|
# "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. 다음 단계
|
||||||
|
|
||||||
|
1. ✅ TDD 테스트 작성 완료 (`test_calendar_natural_language_parsing.py`)
|
||||||
|
2. ⏳ skill-calendar 토큰 갱신 로직 추가
|
||||||
|
3. ⏳ 테스트 실행 및 검증
|
||||||
|
4. ⏳ 배포 및 E2E 테스트
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user