import * as vscode from 'vscode'; /** * Google Calendar (iCal 읽기 전용) 연결 마법사. * * 사용자 흐름: * 1. 이미 셋업 됐으면 "연결 해제 / URL 변경 / 지금 새로고침 / 취소" 선택지 노출 * 2. 새로 셋업: Google Calendar 설정 페이지 외부 브라우저로 열고 → 비공개 iCal URL 입력 * 3. 입력값을 globalState 에 저장 후 즉시 한 번 새로고침 실행 → 캐시 파일 생성 안내 * * OAuth 가 아닌 read-only iCal 만 — 셋업 3분, 토큰 관리 없음. */ export async function runConnectGoogleCalendarIcal(context: vscode.ExtensionContext) { const { readCalendarConfig, writeCalendarConfig, refreshCalendarCache } = await import('../features/calendar'); const cur = readCalendarConfig(context); if (cur.icalUrl) { const choice = await vscode.window.showInformationMessage( `📅 이미 연결됨${cur.lastFetchAt ? ` (마지막 동기화: ${cur.lastFetchAt.slice(0, 16)})` : ''}`, { modal: false }, '지금 새로고침', 'URL 변경', '연결 해제', '취소', ); if (!choice || choice === '취소') return; if (choice === '지금 새로고침') { const r = await refreshCalendarCache(context); if (r.ok) vscode.window.showInformationMessage(`📅 ${r.count}개 일정 동기화 완료.`); else vscode.window.showErrorMessage(r.error || '새로고침 실패'); return; } if (choice === '연결 해제') { await writeCalendarConfig(context, { icalUrl: '', lastFetchAt: undefined }); vscode.window.showInformationMessage('Google Calendar 연결 해제됨. 캐시 파일은 그대로 둡니다.'); return; } // URL 변경 → 아래 입력 흐름으로 fall through } else { const intro = await vscode.window.showInformationMessage( '📅 Google Calendar 연결 (읽기 전용, 셋업 3분)\n\n비공개 iCal URL 1개만 있으면 됩니다. OAuth 없음.\n\n계속할까요?', { modal: true }, '시작', 'Google Calendar 설정 페이지 열기', '취소', ); if (!intro || intro === '취소') return; if (intro === 'Google Calendar 설정 페이지 열기') { await vscode.env.openExternal( vscode.Uri.parse('https://calendar.google.com/calendar/u/0/r/settings'), ); const back = await vscode.window.showInformationMessage( '1. 왼쪽에서 본인 캘린더 클릭 → "캘린더 통합" 섹션\n2. "비공개 주소(iCal 형식)" 옆 복사 버튼 클릭\n3. URL 복사한 뒤 ↓', { modal: true }, '복사함 — URL 붙여넣기', '취소', ); if (back !== '복사함 — URL 붙여넣기') return; } } const url = await vscode.window.showInputBox({ title: 'Google Calendar 비공개 iCal URL', prompt: 'calendar.google.com/calendar/ical/.../private-XXX/basic.ics 형태', placeHolder: 'https://calendar.google.com/calendar/ical/...', value: cur.icalUrl, password: true, ignoreFocusOut: true, validateInput: (v) => { const t = (v || '').trim(); if (!t) return '비어있어요'; if (!/^https?:\/\//.test(t)) return 'http:// 또는 https:// 로 시작해야 합니다.'; return null; }, }); if (!url) return; await writeCalendarConfig(context, { icalUrl: url.trim() }); const r = await refreshCalendarCache(context); if (r.ok) { vscode.window.showInformationMessage( `✅ 연결 완료 — ${r.count}개 일정을 회사 컨텍스트에 동기화했습니다.\n\n이제 기업 모드에서 모든 에이전트가 다가오는 일정을 자동으로 참고합니다.`, ); } else { vscode.window.showErrorMessage( `URL 저장은 됐지만 첫 새로고침 실패: ${r.error}\n\nURL 이 정확한지 다시 확인해주세요.`, ); } } /** * Google Calendar OAuth (쓰기) 연결 마법사. * * iCal 마법사와 별도 — 이쪽은 agent 가 회의록 보고 자동으로 일정 *만들* 수 있게 한다. * 셋업 5~10분: Google Cloud Console 에서 OAuth Client ID/Secret 발급 → 본 마법사가 * loopback OAuth 흐름 실행 → refresh token 받아 globalState 저장. */ export async function runConnectGoogleCalendarOAuth(context: vscode.ExtensionContext) { const { readCalendarConfig, writeCalendarConfig, runOAuthLoopback, fetchUserEmail } = await import('../features/calendar'); const cur = readCalendarConfig(context); const already = !!(cur.clientId && cur.clientSecret && cur.refreshToken); if (already) { const choice = await vscode.window.showInformationMessage( `✅ 이미 OAuth 연결됨${cur.connectedAs ? ` (${cur.connectedAs})` : ''}`, { modal: false }, '재연결', '연결 해제', '취소', ); if (!choice || choice === '취소') return; if (choice === '연결 해제') { await writeCalendarConfig(context, { clientId: undefined, clientSecret: undefined, refreshToken: undefined, accessToken: undefined, accessTokenExpiresAt: undefined, connectedAs: undefined, connectedAt: undefined, }); vscode.window.showInformationMessage( 'OAuth 연결 해제. https://myaccount.google.com/permissions 에서도 권한을 직접 회수할 수 있습니다.', ); return; } // 재연결 → 아래 flow } else { const intro = await vscode.window.showInformationMessage( '📅 Google Calendar 쓰기 연결 (OAuth, 5~10분)\n\n회의록을 받으면 agent 가 자동으로 일정을 생성하게 됩니다.\n\n1단계: Google Cloud Console 에서 OAuth Client ID 발급 (수동 클릭, 가이드 따라)\n2단계: ID + Secret 붙여넣기\n3단계: 브라우저 로그인', { modal: true }, '시작', 'Cloud Console 먼저 열기', '취소', ); if (!intro || intro === '취소') return; if (intro === 'Cloud Console 먼저 열기') { await vscode.env.openExternal(vscode.Uri.parse('https://console.cloud.google.com/apis/credentials')); const back = await vscode.window.showInformationMessage( '아래 절차 마치고 돌아오세요:\n\n1. 새 프로젝트 만들기 (또는 기존)\n2. APIs & Services → Library → "Google Calendar API" 활성화\n3. OAuth 동의 화면 — External, Test users 에 본인 이메일\n4. Credentials → Create OAuth 2.0 Client ID → "Desktop app"\n5. Client ID + Client Secret 복사', { modal: true }, '다 됐음 →', '취소', ); if (back !== '다 됐음 →') return; } } // Settings 에 이미 채워져 있으면 그대로 쓰겠냐고 물어봄 — 매번 똑같은 값 다시 입력하기 귀찮음. const haveBoth = !!(cur.clientId && cur.clientSecret); let clientId: string | undefined = cur.clientId; let clientSecret: string | undefined = cur.clientSecret; if (haveBoth) { const useExisting = await vscode.window.showInformationMessage( `Settings (g1nation.google) 에 이미 Client ID/Secret 이 있습니다.\nID: ${cur.clientId!.slice(0, 20)}…\n\n이 값으로 OAuth 진행할까요?`, { modal: false }, '예 (Settings 값 사용)', '아니오 (새로 입력)', '취소', ); if (useExisting === '취소' || !useExisting) return; if (useExisting === '아니오 (새로 입력)') { clientId = undefined; clientSecret = undefined; } } if (!clientId) { clientId = await vscode.window.showInputBox({ title: 'Google OAuth Client ID', prompt: 'Credentials 페이지에서 복사한 Client ID — 자동으로 Settings(g1nation.google.clientId)에 저장됨', placeHolder: 'xxxxxxxx.apps.googleusercontent.com', value: cur.clientId, ignoreFocusOut: true, validateInput: (v) => (v || '').trim() ? null : '비어있어요', }); if (!clientId) return; } if (!clientSecret) { clientSecret = await vscode.window.showInputBox({ title: 'Google OAuth Client Secret', prompt: '같은 화면의 Client Secret — Settings(g1nation.google.clientSecret)에 저장됨', placeHolder: 'GOCSPX-...', value: cur.clientSecret, password: true, ignoreFocusOut: true, validateInput: (v) => (v || '').trim() ? null : '비어있어요', }); if (!clientSecret) return; } await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: '🔐 Google 로그인 대기 중…', cancellable: true, }, async (progress, cancelToken) => { progress.report({ message: '브라우저에서 Google 로그인 진행하세요 (최대 5분 대기)' }); const result = await runOAuthLoopback(clientId.trim(), clientSecret.trim(), cancelToken); if (!result.ok) { vscode.window.showErrorMessage(`OAuth 실패: ${result.error}`); return; } const email = await fetchUserEmail(result.accessToken); await writeCalendarConfig(context, { clientId: clientId.trim(), clientSecret: clientSecret.trim(), refreshToken: result.refreshToken, accessToken: result.accessToken, accessTokenExpiresAt: result.expiresAt, calendarId: cur.calendarId ?? 'primary', defaultDurationMinutes: cur.defaultDurationMinutes ?? 60, connectedAs: email, connectedAt: new Date().toISOString(), }); vscode.window.showInformationMessage( `✅ Google Calendar 쓰기 연결 완료!${email ? ' (' + email + ')' : ''}\n\n이제 회의록을 보내거나 due 가 있는 작업을 알려주면 agent 가 자동으로 일정을 생성합니다.`, ); }); }