구글 계정으로 로그인
구글 계정으로 로그인을 하기 위해서는 먼저 구글 계정 연결을 해야 합니다.
구글 계정 연결은 계정 설정 페이지에서 하고, 구글 계정 로그인은 로그인 창에 둡니다.
구글 계정 로그인
구글 계정 연동
Vuetify로 계정 설정 페이지 "Google 계정 연동" 버튼을 추가하고,
클릭 시 signInWithPopup으로 구글 계정에 로그인 하여 uid를 가져옵니다.
가져온 uid를 Firestore의 profiles 컬렉션 내 uids[] 배열에 추가합니다.
여기까지 진행하면 구글 계정 연동이 된 것입니다.
Profile.vue의 script
// ✅ Google 계정 연동
const linkGoogleAccount = async () => {
try {
// 구글 로그인 → uid 반환
await auth.signInWithGoogle();
// uids 배열에 uid 추가
await auth.linkOAuthToProfile(auth.user.uid);
alert('Google 계정이 연동되었습니다.');
} catch (error) {
console.error('Google 연동 오류:', error);
alert('Google 연동 실패: ' + error.message);
}
};
authStore
const signInWithGoogle = async () => {
try {
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(auth, provider);
user.value = result.user;
console.log("Google 로그인 성공:", user.value);
} catch (error) {
console.error("Google 로그인 실패:", error.message);
throw error;
}
};
const linkOAuthToProfile = async (oauthUid) => {
if (!profile.value || !profile.value.id) {
console.warn('프로필이 설정되지 않았습니다.');
return;
}
const profileRef = doc(db, 'profiles', profile.value.id)
// uids 배열에 해당 uid가 없으면 추가
if (!profile.value.uids.includes(oauthUid)) {
try {
await updateDoc(profileRef, {
uids: arrayUnion(oauthUid),
});
profile.value.uids.push(oauthUid); // 로컬에도 반영
console.log('OAuth UID가 uids 배열에 추가되었습니다.');
} catch (error) {
console.error('UID 추가 실패:', error);
}
} else {
console.log('이미 존재하는 UID입니다.');
}
};
구글 계정 로그인
로그인 창에서 구글 계정 로그인을 클릭하면
signInWithPopup 함수로 로그인한 후 uid를 가져오면
profile 문서의 uids[] 필드에 uid가 포함된 프로필 정보를 가져옵니다.
그러면 로그인은 성공한 것입니다.
Login.vue의 script
// ✅ Google 로그인 로직
const onGoogleLogin = async () => {
try {
// popup 로그인
await auth.signInWithGoogle();
// Firestore에서 profile 가져오기
await auth.fetchProfile(auth.user.uid);
} catch (error) {
console.error('Google 로그인 실패:', error);
alert('Google 로그인에 실패했습니다.');
}
};
authStore - 사용자 프로필 가져오기
// 모든 사용자의 프로필 읽기
const fetchProfiles = async () => {
try {
profiles.length = 0;
const profileRef = collection(db, 'profiles');
const querySnapshot = await getDocs(profileRef);
querySnapshot.forEach(doc => profiles.push({ id: doc.id, ...doc.data() }));
if (user.value) {
const foundProfile = profiles.find(p => p.uids.includes(user.value.uid));
if (foundProfile) {
fetchProfile(foundProfile.id);
}
}
} catch (error) {
console.log('fetchProfiles: ' + error.message);
}
};
// profile 정보 읽기
const fetchProfile = async (profileId) => {
try {
const profileRef = doc(db, 'profiles', profileId);
const profileSnap = await getDoc(profileRef);
if (profileSnap.exists()) {
profile.value = { id: profileId, ...profileSnap.data() };
} else {
console.log(`fetchProfile: ${profileId} 계정 설정 정보가 없습니다.`);
}
} catch (error) {
alert('Failed to fetch user: ' + error.message);
}
};
Login.vue
<!-- src/views/Login.vue -->
<template>
<v-container fluid>
<v-row align="center" justify="center">
<v-col cols="12" sm="10" md="6">
<v-card elevation="10" class="pa-4">
<v-card-title class="text-h6 font-weight-bold">로그인</v-card-title>
<v-card-text>
<v-form @submit.prevent="onLogin" v-model="formValid" ref="formRef">
<v-text-field
v-model="email"
label="이메일"
type="email"
:rules="emailRules"
prepend-inner-icon="mdi-email"
required
/>
<v-text-field
v-model="password"
label="비밀번호"
type="password"
:rules="passwordRules"
prepend-inner-icon="mdi-lock"
required
/>
<v-btn :loading="auth.loading" type="submit" color="primary" class="mt-4" block>
로그인
</v-btn>
</v-form>
<v-btn variant="text" color="secondary" @click="passwordReset" class="mt-2" block>
비밀번호 재설정
</v-btn>
<!-- 🔹 Google 로그인 버튼 -->
<v-divider class="my-2" />
<v-btn
color="red"
variant="outlined"
block
prepend-icon="mdi-google"
@click="onGoogleLogin"
>
Google 계정으로 로그인
</v-btn>
</v-card-text>
<v-card-actions class="justify-center">
<RouterLink to="/register">계정이 없으신가요?</RouterLink>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref } from 'vue';
import { useAuthStore } from '@/stores/authStore';
const auth = useAuthStore();
const email = ref('');
const password = ref('');
const formRef = ref(null);
const formValid = ref(false);
const emailRules = [
(v) => !!v || '이메일을 입력해주세요.',
(v) => /.+@.+\..+/.test(v) || '올바른 이메일 형식이 아닙니다.',
];
const passwordRules = [
(v) => !!v || '비밀번호를 입력해주세요.',
(v) => v.length >= 6 || '비밀번호는 최소 6자 이상이어야 합니다.',
];
const onLogin = () => {
if (formRef.value?.validate()) {
auth.login(email.value, password.value);
}
};
const passwordReset = async () => {
if (!email.value) {
alert('비밀번호 재설정을 위해 이메일을 입력해주세요.');
return;
}
await auth.resetPassword(email.value);
};
// ✅ Google 로그인 로직
const onGoogleLogin = async () => {
try {
// popup 로그인
await auth.signInWithGoogle();
// Firestore에서 profile 가져오기
await auth.fetchProfile(auth.user.uid);
} catch (error) {
console.error('Google 로그인 실패:', error);
alert('Google 로그인에 실패했습니다.');
}
};
</script
Profile.vue
<!-- src/views/Profile.vue -->
<template>
<v-container fluid>
<v-row align="center" justify="center">
<v-col cols="12" sm="10" md="6">
<v-card class="pa-2" elevation="8">
<v-card-title class="text-h5 font-weight-bold">
계정 설정
</v-card-title>
<v-card-text>
<v-form @submit.prevent="saveProfile" v-model="formValid">
<v-text-field v-model="email" label="이메일" readonly disabled/>
<v-text-field v-model="name" label="이름 또는 별명" />
<v-textarea v-model="aboutMe" label="자기소개" rows="1" />
<v-btn :loading="auth.loading" type="submit" color="primary" block>
저장
</v-btn>
</v-form>
</v-card-text>
<!-- Google 계정 연동 -->
<div class="pl-4 pr-4">
<v-btn color="red darken-1" @click="linkGoogleAccount" block>
<v-icon start>mdi-google</v-icon>
Google 계정 연동
</v-btn>
</div>
<v-divider class="my-2"></v-divider>
<v-card-subtitle class="text-subtitle-1 font-weight-bold mb-2">
비밀번호 변경
</v-card-subtitle>
<v-form @submit.prevent="handleChangePassword" v-model="passwordFormValid">
<v-text-field
v-model="currentPassword"
label="현재 비밀번호"
type="password"
required
/>
<v-text-field
v-model="newPassword"
label="새 비밀번호"
type="password"
required
/>
<v-text-field
v-model="confirmPassword"
label="새 비밀번호 확인"
type="password"
required
/>
<v-btn type="submit" color="primary" block class="mt-0">
비밀번호 변경
</v-btn>
</v-form>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useAuthStore } from '@/stores/authStore';
const auth = useAuthStore();
const formValid = ref(false);
const passwordFormValid = ref(false);
const id = ref('');
const email = ref('');
const name = ref('');
const status = ref('');
const aboutMe = ref('');
const currentPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
// 프로필 정보를 로드하여 필드에 채우기
watch(
() => auth.profile,
(profile) => {
if (profile) {
id.value = profile.id;
email.value = profile.email;
name.value = profile.name || '';
status.value = profile.status || '';
aboutMe.value = profile.aboutMe || '';
}
},
{ immediate: true }
);
// 프로필 정보 저장
const saveProfile = async () => {
if (!auth.profile) return;
await auth.updateProfile({
id : auth.profile.id,
name: name.value,
status: status.value,
aboutMe: aboutMe.value,
});
};
// 비밀번호 변경 처리
const handleChangePassword = async () => {
if (newPassword.value !== confirmPassword.value) {
alert('새 비밀번호가 일치하지 않습니다.');
return;
}
try {
await auth.changePassword(currentPassword.value, newPassword.value);
alert('비밀번호가 변경되었습니다.');
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
} catch (error) {
alert('비밀번호 변경 실패: ' + error.message);
}
};
// ✅ Google 계정 연동
const linkGoogleAccount = async () => {
try {
// 구글 로그인 → uid 반환
await auth.signInWithGoogle();
// uids 배열에 uid 추가
await auth.linkOAuthToProfile(auth.user.uid);
alert('Google 계정이 연동되었습니다.');
} catch (error) {
console.error('Google 연동 오류:', error);
alert('Google 연동 실패: ' + error.message);
}
};
</script>
authStore
// src/stores/authStore.js
import { defineStore } from 'pinia';
import { ref, reactive, computed } from 'vue';
import { useRouter } from 'vue-router'
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
sendPasswordResetEmail,
updatePassword,
EmailAuthProvider,
reauthenticateWithCredential
} from 'firebase/auth';
import {
collection, doc, addDoc, setDoc,
getDoc, updateDoc, getDocs,
} from 'firebase/firestore';
import { auth, db } from '@/firebase';
export const useAuthStore = defineStore('auth', () => {
const router = useRouter();
const user = ref(null);
const loading = ref(false);
const profile = ref(null);
const profiles = reactive([]);
const isAuthenticated = computed(() => !!user.value);
// 회원가입
const register = async (email, password) => {
try {
loading.value = true;
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
user.value = userCredential.user;
// 계정 만들기 후 Profile 저장
const profileData = {
userId: userCredential.user.uid,
email,
name: '',
status: '',
aboutMe: '',
createdAt: new Date(),
uids: [userCredential.user.uid],
};
await registerProfile(profileData);
router.push('/profile'); // 프로필 수정으로
} catch (error) {
alert('회원가입 실패: ' + error.message);
} finally {
loading.value = false;
}
};
// firestore에 계정 설정 저장
const registerProfile = async (profileData) => {
if (!user) return;
try {
const profileRef = doc(db, 'profiles', user.value.uid);
await setDoc(profileRef, profileData);
await fetchProfiles();
} catch (error) {
alert('Failed to register user: ' + error);
}
};
// 모든 사용자의 프로필 읽기
const fetchProfiles = async () => {
try {
profiles.length = 0;
const profileRef = collection(db, 'profiles');
const querySnapshot = await getDocs(profileRef);
querySnapshot.forEach(doc => profiles.push({ id: doc.id, ...doc.data() }));
if (user.value) {
const foundProfile = profiles.find(p => p.uids.includes(user.value.uid));
if (foundProfile) {
fetchProfile(foundProfile.id);
}
}
} catch (error) {
console.log('fetchProfiles: ' + error.message);
}
};
// profile 정보 읽기
const fetchProfile = async (profileId) => {
try {
const profileRef = doc(db, 'profiles', profileId);
const profileSnap = await getDoc(profileRef);
if (profileSnap.exists()) {
profile.value = { id: profileId, ...profileSnap.data() };
} else {
console.log(`fetchProfile: ${profileId} 계정 설정 정보가 없습니다.`);
}
} catch (error) {
alert('Failed to fetch user: ' + error.message);
}
};
// 계정 설정 수정
const updateProfile = async (updatedProfile) => {
try {
loading.value = true;
const profileDocRef = doc(db, 'profiles', updatedProfile.id);
await updateDoc(profileDocRef, {
name: updatedProfile.name,
status: updatedProfile.status,
aboutMe: updatedProfile.aboutMe,
// 다른 필드도 필요에 따라 추가 가능
});
profile.value = { ...updatedProfile }; // 로컬 상태도 반영
loading.value = false;
alert('계정 정보를 수정했습니다.');
} catch (error) {
loading.value = false;
alert('계정 정보 수정 중 오류 발생: ' + error.message);
}
}
// 로그인
const login = async (email, password) => {
try {
loading.value = true;
const userCredential = await signInWithEmailAndPassword(auth, email, password);
user.value = userCredential.user;
fetchProfiles();
router.push('/');
} catch (error) {
alert('로그인 실패: ' + error.message);
} finally {
loading.value = false;
}
};
// 로그아웃
const logout = async () => {
try {
if (user.value) {
await signOut(auth);
router.push('/');
user.value = null;
}
} catch (error) {
alert('로그아웃 실패: ' + error.message);
}
};
// 로그인 상태 유지
const initializeAuth = () => {
onAuthStateChanged(auth, async (currentUser) => {
user.value = currentUser;
if (currentUser) {
fetchProfiles();
}
});
};
// 비밀번호 재설정
const resetPassword = async (email) => {
try {
await sendPasswordResetEmail(auth, email);
alert('비밀번호 재설정 이메일을 보냈습니다.');
} catch (error) {
alert('재설정 실패: ' + error.message);
}
};
// 비밀번호 변경
const changePassword = async (currentPassword, newPassword) => {
try {
const credential = EmailAuthProvider.credential(auth.currentUser.email, currentPassword);
await reauthenticateWithCredential(auth.currentUser, credential);
await updatePassword(auth.currentUser, newPassword);
alert('비밀번호가 변경되었습니다.');
} catch (error) {
alert('비밀번호 변경 실패: ' + error.message);
}
};
const signInWithGoogle = async () => {
try {
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(auth, provider);
user.value = result.user;
console.log("Google 로그인 성공:", user.value);
} catch (error) {
console.error("Google 로그인 실패:", error.message);
throw error;
}
};
const linkOAuthToProfile = async (oauthUid) => {
if (!profile.value || !profile.value.id) {
console.warn('프로필이 설정되지 않았습니다.');
return;
}
const profileRef = doc(db, 'profiles', profile.value.id);
// uids 배열에 해당 uid가 없으면 추가
if (!profile.value.uids.includes(oauthUid)) {
try {
await updateDoc(profileRef, {
uids: arrayUnion(oauthUid),
});
profile.value.uids.push(oauthUid); // 로컬에도 반영
console.log('OAuth UID가 uids 배열에 추가되었습니다.');
} catch (error) {
console.error('UID 추가 실패:', error);
}
} else {
console.log('이미 존재하는 UID입니다.');
}
};
return {
user,
isAuthenticated,
loading,
profile,
profiles,
register,
updateProfile,
fetchProfile,
login,
logout,
initializeAuth,
resetPassword,
changePassword,
signInWithGoogle,
linkOAuthToProfile
};
});
'Vue 3 + Firebase 기반 실시간 채팅 앱 개발' 카테고리의 다른 글
11. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 실시간 채팅 (0) | 2025.05.09 |
---|---|
10. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 새채팅 (0) | 2025.05.08 |
8. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 비밀번호 변경, 비밀번호 재설정 (0) | 2025.05.08 |
7. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 로그인, 로그아웃, 자동 로그인 (0) | 2025.05.07 |
6. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 계정 만들기 및 계정 정보 설정 (0) | 2025.05.06 |