// Google OAuth 및 사용자 데이터 서비스 export interface GoogleUser { id: string email: string name: string picture: string locale: string } export interface SavedPlace { place_id: string name: string formatted_address: string geometry: { location: { lat: number lng: number } } rating?: number user_ratings_total?: number types: string[] photos?: google.maps.places.PlacePhoto[] saved_lists: string[] // 어떤 리스트에 저장되었는지 user_rating?: number // 사용자가 준 평점 user_review?: string // 사용자 리뷰 visited?: boolean // 방문 여부 } export interface GoogleMyMap { id: string title: string description?: string places: SavedPlace[] created_date: string updated_date: string } class GoogleAuthService { private gapi: any = null private auth2: any = null private isInitialized = false // Google API 초기화 async initialize(clientId: string) { return new Promise((resolve, reject) => { if (typeof window === 'undefined') { reject(new Error('Google Auth는 브라우저에서만 사용 가능합니다')) return } // Google API 스크립트 로드 if (!window.gapi) { const script = document.createElement('script') script.src = 'https://apis.google.com/js/api.js' script.onload = () => this.loadGapi(clientId, resolve, reject) script.onerror = () => reject(new Error('Google API 스크립트 로드 실패')) document.head.appendChild(script) } else { this.loadGapi(clientId, resolve, reject) } }) } private loadGapi(clientId: string, resolve: () => void, reject: (error: Error) => void) { window.gapi.load('auth2', () => { window.gapi.auth2.init({ client_id: clientId, scope: [ 'profile', 'email', 'https://www.googleapis.com/auth/maps.readonly', // Google Maps 데이터 읽기 'https://www.googleapis.com/auth/mymaps.readonly' // My Maps 읽기 ].join(' ') }).then(() => { this.gapi = window.gapi this.auth2 = window.gapi.auth2.getAuthInstance() this.isInitialized = true resolve() }).catch((error: any) => { reject(new Error(`Google Auth 초기화 실패: ${error.error || error}`)) }) }) } // 로그인 async signIn(): Promise { if (!this.isInitialized) { throw new Error('Google Auth가 초기화되지 않았습니다') } try { const authResult = await this.auth2.signIn() const profile = authResult.getBasicProfile() const user: GoogleUser = { id: profile.getId(), email: profile.getEmail(), name: profile.getName(), picture: profile.getImageUrl(), locale: profile.getLocale() || 'ko' } // 로컬 스토리지에 사용자 정보 저장 localStorage.setItem('google_user', JSON.stringify(user)) return user } catch (error) { throw new Error(`로그인 실패: ${error}`) } } // 로그아웃 async signOut(): Promise { if (!this.isInitialized) return try { await this.auth2.signOut() localStorage.removeItem('google_user') } catch (error) { console.error('로그아웃 실패:', error) } } // 현재 로그인된 사용자 정보 getCurrentUser(): GoogleUser | null { const userStr = localStorage.getItem('google_user') return userStr ? JSON.parse(userStr) : null } // 로그인 상태 확인 isSignedIn(): boolean { return this.isInitialized && this.auth2?.isSignedIn.get() && this.getCurrentUser() !== null } // 저장된 장소 가져오기 (Google My Maps API 사용) async getSavedPlaces(): Promise { if (!this.isSignedIn()) { throw new Error('로그인이 필요합니다') } try { // Google My Maps API 호출 const response = await this.gapi.client.request({ path: 'https://www.googleapis.com/mymaps/v1/maps', method: 'GET' }) const savedPlaces: SavedPlace[] = [] // My Maps에서 장소 추출 if (response.result && response.result.maps) { for (const map of response.result.maps) { const mapPlaces = await this.getPlacesFromMap(map.id) savedPlaces.push(...mapPlaces) } } return savedPlaces } catch (error) { console.error('저장된 장소 가져오기 실패:', error) // 폴백: 로컬 스토리지에서 이전에 저장된 데이터 사용 return this.getCachedSavedPlaces() } } // 특정 My Map에서 장소들 가져오기 private async getPlacesFromMap(mapId: string): Promise { try { const response = await this.gapi.client.request({ path: `https://www.googleapis.com/mymaps/v1/maps/${mapId}/features`, method: 'GET' }) const places: SavedPlace[] = [] if (response.result && response.result.features) { for (const feature of response.result.features) { if (feature.geometry && feature.geometry.location) { const place: SavedPlace = { place_id: feature.properties?.place_id || `custom_${Date.now()}`, name: feature.properties?.name || 'Unknown Place', formatted_address: feature.properties?.address || '', geometry: { location: { lat: feature.geometry.location.latitude, lng: feature.geometry.location.longitude } }, types: feature.properties?.types || [], saved_lists: [mapId], user_rating: feature.properties?.rating, user_review: feature.properties?.description } places.push(place) } } } return places } catch (error) { console.error(`Map ${mapId}에서 장소 가져오기 실패:`, error) return [] } } // My Maps 목록 가져오기 async getMyMaps(): Promise { if (!this.isSignedIn()) { throw new Error('로그인이 필요합니다') } try { const response = await this.gapi.client.request({ path: 'https://www.googleapis.com/mymaps/v1/maps', method: 'GET' }) const myMaps: GoogleMyMap[] = [] if (response.result && response.result.maps) { for (const map of response.result.maps) { const places = await this.getPlacesFromMap(map.id) const myMap: GoogleMyMap = { id: map.id, title: map.title || 'Untitled Map', description: map.description, places: places, created_date: map.createTime, updated_date: map.updateTime } myMaps.push(myMap) } } // 로컬 캐시에 저장 localStorage.setItem('google_my_maps', JSON.stringify(myMaps)) return myMaps } catch (error) { console.error('My Maps 가져오기 실패:', error) // 폴백: 캐시된 데이터 사용 return this.getCachedMyMaps() } } // 캐시된 저장된 장소 가져오기 private getCachedSavedPlaces(): SavedPlace[] { const cached = localStorage.getItem('google_saved_places') return cached ? JSON.parse(cached) : [] } // 캐시된 My Maps 가져오기 private getCachedMyMaps(): GoogleMyMap[] { const cached = localStorage.getItem('google_my_maps') return cached ? JSON.parse(cached) : [] } // 저장된 장소를 일정에 추가하기 쉬운 형태로 변환 convertToItineraryFormat(savedPlace: SavedPlace) { return { id: `saved_${savedPlace.place_id}`, title: savedPlace.name, location: savedPlace.formatted_address, description: savedPlace.user_review || `Google Maps에서 가져온 장소 (평점: ${savedPlace.user_rating || savedPlace.rating || 'N/A'})`, type: this.inferActivityType(savedPlace.types), coordinates: { lat: savedPlace.geometry.location.lat, lng: savedPlace.geometry.location.lng }, time: '09:00', // 기본 시간 source: 'google_maps' } } // 장소 타입에서 활동 타입 추론 private inferActivityType(types: string[]): 'attraction' | 'food' | 'accommodation' | 'transport' | 'other' { if (types.includes('restaurant') || types.includes('food') || types.includes('meal_takeaway')) { return 'food' } if (types.includes('lodging')) { return 'accommodation' } if (types.includes('tourist_attraction') || types.includes('museum') || types.includes('park')) { return 'attraction' } if (types.includes('transit_station') || types.includes('airport')) { return 'transport' } return 'other' } } // 싱글톤 인스턴스 export const googleAuthService = new GoogleAuthService() // Google OAuth 설정 export const GoogleAuthConfig = { // 실제 사용시에는 환경변수로 관리 CLIENT_ID: import.meta.env.VITE_GOOGLE_OAUTH_CLIENT_ID || '', // 필요한 권한 범위 SCOPES: [ 'profile', 'email', 'https://www.googleapis.com/auth/maps.readonly', 'https://www.googleapis.com/auth/mymaps.readonly' ], // 지원하는 기능들 FEATURES: { MY_MAPS: true, // Google My Maps 지원 SAVED_PLACES: true, // 저장된 장소 지원 REVIEWS: false, // 리뷰 데이터 (제한적) LOCATION_HISTORY: false // 위치 기록 (프라이버시 이슈) } } as const