프로필 수정 버튼을 클릭하면
모달을 띄워서
프로필을 수정할 수 있게끔 기능을 만들어본다.
기존에 사용하던 TweetModal.vue의 내용들을 복사해서
ProfileEdeitModal.vue를 생성한다.
안의 html 부분들을 필요에 맞게 변경해주는데
<template>
<div
class="relative z-10"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
@click="$emit('close-modal')"
>
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
></div>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
>
<div
@click.stop
class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
>
<div class="bg-white">
<div
class="flex items-center justify-between border-b border-gray-100 p-2"
>
<div class="flex items-center">
<button
@click="$emit('close-modal')"
class="flex items-center justify-center fas fa-times text-primary text-lg p-2 h-10 w-10 hover:bg-blue-50"
></button>
<span class="font-bold text-lg">프로필 수정</span>
</div>
<div class="text-right mr-2">
<button
class="hover:bg-dark bg-primary text-white font-bold px-3 py-1 rounded-full"
>
저장
</button>
</div>
</div>
<!-- 트윗팅 섹션 -->
<div class="h-60">
<!-- background image -->
<div
class="bg-gray-300 h-40 relative flex-none flex items-center justify-center"
>
<img
src="/background.png"
ref="backgroundImage"
class="object-cover absolute h-full w-full"
/>
<button
@click="onChangeBackgroundImage"
class="absolute h-10 w-10 hover:text-gray-200 rounded-full fas fa-camera text-white text-lg"
></button>
<input
@change="previewBackgroundImage"
type="file"
accept="image/"
id="backgroundImageInput"
class="hidden"
/>
<!-- profile image -->
<img
src="/profile.jpeg"
ref="profileImage"
class="border-4 border-white w-28 h-28 absolute -bottom-14 left-2 rounded-full"
/>
<button
@click="onChangeProfileImage"
class="absolute h-10 w-10 -bottom-5 left-11 hover:text-gray-200 rounded-full fas fa-camera text-white text-lg"
></button>
<input
@change="previewProfileImage"
type="file"
accept="image/"
id="profileImageInput"
class="hidden"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
현재 필요한 기능들은 아래와 같다.
1. 백그라운드의 카메라 버튼을 클릭하면 파일을 선택할 수 있게 만들고, 그 사진을 백그라운드에 적용시키는것
2. 백그라운드와 마찬가지로 프로필 파일을 선택 후 바로 프로필에 사진이 노출될 수 있도록 만들어야함
html 구성은 아래와 같다.
<!-- background image -->
<div
class="bg-gray-300 h-40 relative flex-none flex items-center justify-center"
>
<img
src="/background.png"
ref="backgroundImage"
class="object-cover absolute h-full w-full"
/>
<button
@click="onChangeBackgroundImage"
class="absolute h-10 w-10 hover:text-gray-200 rounded-full fas fa-camera text-white text-lg"
></button>
<input
@change="previewBackgroundImage"
type="file"
accept="image/"
id="backgroundImageInput"
class="hidden"
/>
button은 카메라 아이콘을 말하고
input은 위의 버튼을 클릭하면 이 input이 실행되어야 함
그래서 버튼에는 click 이벤트로 onChangeBackGroundImage 함수를 실행하게 만든다.
이 함수는
input에 id를 주어
javascript를 활용해서 실행되게 할거임
input에는 accept 를 통해 image파일만 입력할 수 있도록 설정한다.
함수는 간단함
onChangeBackgorundImage가 클릭되면
id가 backgroundImageInput을 실행시키도록 한다. *(즉 input)
const onChangeBackgroundImage = () => {
document.getElementById("backgroundImageInput").click();
};
테스트를 해본다.
정상적으로 작동하는것을 확인할 수 있다.
하지만 현재로써 파일을 선택한다고 해도
background에 바로 반영되지 않는다.
이를 위해서
img태그에 ref를 주어
input의 파일이 변경될 때 마다
src의 값을 변경하면됨
ref에 backgroundImage를 줬다.
<div
class="bg-gray-300 h-40 relative flex-none flex items-center justify-center"
>
<img
src="/background.png"
ref="backgroundImage"
class="object-cover absolute h-full w-full"
/>
input에는 change 함수로 파일이 변경될 때마다 previewBackgroundImage가 실행되게 한다.
<input
@change="previewBackgroundImage"
type="file"
accept="image/"
id="backgroundImageInput"
class="hidden"
/>
상태로 관리하기 위해서 초기값은 null로 설정한다.
const backgroundImage = ref(null);
const profileImage = ref(null);
previewBackgroundImage는 아래와 같다.
이거는 그대로 사용하면됨
const previewBackgroundImage = (e) => {
const file = e.target.files[0];
let reader = new FileReader();
reader.onload = function (e) {
backgroundImage.value.src = e.target.result;
};
reader.readAsDataURL(file);
};
profile도 동일하게 활용해서 만들면됨
전체 코드
<template>
<div
class="relative z-10"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
@click="$emit('close-modal')"
>
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
></div>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
>
<div
@click.stop
class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
>
<div class="bg-white">
<div
class="flex items-center justify-between border-b border-gray-100 p-2"
>
<div class="flex items-center">
<button
@click="$emit('close-modal')"
class="flex items-center justify-center fas fa-times text-primary text-lg p-2 h-10 w-10 hover:bg-blue-50"
></button>
<span class="font-bold text-lg">프로필 수정</span>
</div>
<div class="text-right mr-2">
<button
class="hover:bg-dark bg-primary text-white font-bold px-3 py-1 rounded-full"
>
저장
</button>
</div>
</div>
<!-- 트윗팅 섹션 -->
<div class="h-60">
<!-- background image -->
<div
class="bg-gray-300 h-40 relative flex-none flex items-center justify-center"
>
<img
src="/background.png"
ref="backgroundImage"
class="object-cover absolute h-full w-full"
/>
<button
@click="onChangeBackgroundImage"
class="absolute h-10 w-10 hover:text-gray-200 rounded-full fas fa-camera text-white text-lg"
></button>
<input
@change="previewBackgroundImage"
type="file"
accept="image/"
id="backgroundImageInput"
class="hidden"
/>
<!-- profile image -->
<img
src="/profile.jpeg"
ref="profileImage"
class="border-4 border-white w-28 h-28 absolute -bottom-14 left-2 rounded-full"
/>
<button
@click="onChangeProfileImage"
class="absolute h-10 w-10 -bottom-5 left-11 hover:text-gray-200 rounded-full fas fa-camera text-white text-lg"
></button>
<input
@change="previewProfileImage"
type="file"
accept="image/"
id="profileImageInput"
class="hidden"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, computed } from "vue";
import addTweet from "../utils/addTweet";
import store from "../store";
export default {
setup(props, { emit }) {
const tweetBody = ref("");
const currentUser = computed(() => store.state.user);
const backgroundImage = ref(null);
const profileImage = ref(null);
//트윗 추가 함수 -> utils의 addTweet함수
const onAddTweet = async () => {
try {
addTweet(tweetBody.value, currentUser.value);
tweetBody.value = "";
emit("close-modal"); //상위 컴포넌트에 close-modal실행
} catch (e) {
console.log("트윗 에러 메시지", e);
}
};
const onChangeBackgroundImage = () => {
document.getElementById("backgroundImageInput").click();
};
const onChangeProfileImage = () => {
document.getElementById("profileImageInput").click();
};
const previewBackgroundImage = (e) => {
const file = e.target.files[0];
let reader = new FileReader();
reader.onload = function (e) {
backgroundImage.value.src = e.target.result;
};
reader.readAsDataURL(file);
};
const previewProfileImage = (e) => {
const file = e.target.files[0];
let reader = new FileReader();
reader.onload = function (e) {
profileImage.value.src = e.target.result;
};
reader.readAsDataURL(file);
};
return {
tweetBody,
onAddTweet,
currentUser,
onChangeBackgroundImage,
onChangeProfileImage,
previewBackgroundImage,
backgroundImage,
profileImage,
previewProfileImage,
};
},
};
</script>
그리고 프로필 아래에
유저 정보를 나타낼수 있는 input 태그를 추가했다.
<div class="flex flex-col p-2">
<div
class="mx-2 my-1 px-2 py-1 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<div class="text-sm">이름</div>
<input
type="text"
:value="currentUser.email"
class="text-black focus:outline-none"
/>
</div>
<div
class="mx-2 my-1 px-2 py-5 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<input
type="text"
class="text-black focus:outline-none"
placeholder="자기소개"
/>
</div>
<div
class="mx-2 my-1 px-2 py-1 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<input
type="text"
class="text-black focus:outline-none"
placeholder="위치"
/>
</div>
<div
class="mx-2 my-1 px-2 py-3 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<input
type="text"
class="text-black focus:outline-none"
placeholder="웹사이트"
/>
</div>
</div>
이제 저장 버튼을 클릭하면
firebase의 storage에 저장한 뒤
firestore에 사진 url을 저장해 보도록 함
저장 버튼에 클릭 이벤트를 걸어준다.
<button
@click="onSaveProfile"
class="hover:bg-dark bg-primary text-white font-bold px-3 py-1 rounded-full"
>
저장
</button>
const backgroundImage = ref(null);
const backgroundImageData = ref(null);
const profileImageData = ref(null);
const profileImage = ref(null);
기존에 작성한 previewBackgroundImage 와 previewProfileImage에서
file을
profileImageData 에 저장하여
const previewBackgroundImage = (e) => {
const file = e.target.files[0];
let reader = new FileReader();
//이미지 선택되었을 때
backgroundImageData.value = file;
reader.onload = function (e) {
backgroundImage.value.src = e.target.result;
};
reader.readAsDataURL(file);
};
const previewProfileImage = (e) => {
const file = e.target.files[0];
let reader = new FileReader();
//이미지 선택되었을 때
profileImageData.value = file;
reader.onload = function (e) {
profileImage.value.src = e.target.result;
};
reader.readAsDataURL(file);
};
그 데이터 값이 존재할 경우에만
버튼이 실행된다. 아니면 return
그리고 try catch문으로 에러 핸들링을 해주고
if문으로 변경된 데이터를 스토리지에 저장한다.
https://firebase.google.com/docs/storage/web/create-reference?hl=ko&_gl=1*op9hxf*_up*MQ..*_ga*MjE1NzUwMjc0LjE3MTI4OTQ4MDg.*_ga_CW55HF8NVT*MTcxMjg5NDgwNy4xLjAuMTcxMjg5NDgwNy4wLjAuMA..
그리고 스토리지에 저장 한 이후에
url을 받아와서
db에 유저 정보에 업데이트해줌
const onSaveProfile = async () => {
if (!profileImageData.value && !backgroundImageData.value) {
return;
}
try {
if (profileImageData.value) {
const profileStorageRef = storageRef(
storage,
`profile/${currentUser.value.uid}/profile`
);
const profileSnapshot = await uploadBytes(
profileStorageRef,
profileImageData.value
);
const profileImageUrl = await getDownloadURL(profileSnapshot.ref);
const userDocRef = doc(db, "users", currentUser.value.uid);
await updateDoc(userDocRef, { profile_image_url: profileImageUrl });
store.commit("SET_PROFILE_IMAGE", profileImageUrl);
}
} catch (e) {
console.error("Error uploading profile images:", error);
}
try {
if (backgroundImageData.value) {
const backgroundStorageRef = storageRef(
storage,
`profile/${currentUser.value.uid}/background`
);
const backgroundSnapshot = await uploadBytes(
backgroundStorageRef,
backgroundImageData.value
);
const backgroundImageUrl = await getDownloadURL(
backgroundSnapshot.ref
);
const userDocRef = doc(db, "users", currentUser.value.uid);
await updateDoc(userDocRef, {
background_image_url: backgroundImageUrl,
});
store.commit("SET_BACKGROUND_IMAGE", backgroundImageUrl);
}
} catch (e) {
console.error("Error uploading profile images:", error);
}
emit("close-modal");
};
모든 작업이 끝나면 모달창을 닫기 위해
emit("close-modal)을 사용하여 상위 컴포넌트에 전달한다.
상위 컴포넌트인 Profile 에서는
close-modal이 실행되면 showProfileEditModal을 false로 만들어서 모달창을 닫아준다
<profile-edit-modal
v-if="showProfileEditModal"
@close-modal="showProfileEditModal = false"
/>
그리고 이렇게 업데이트 된 정보는
로그아웃 후 재 접속해야 바뀐걸 확인할 수 있게 되어서
store에서 전역 상태로 background와 profile image를 관리하여
실시간으로 반영되게 만들어줌
store-> index.js
import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";
const store = createStore({
state() {
return {
user: null,
};
},
mutations: {
SET_USERS: (state, user) => {
state.user = user;
},
SET_BACKGROUND_IMAGE: (state, image) => {
state.user.background_image_url = image;
},
SET_PROFILE_IMAGE: (state, image) => {
state.user.profile_image_url = image;
},
},
plugins: [createPersistedState()],
});
export default store;
저장한 후
테스트 해본다
전체 코드
<template>
<div
class="relative z-10"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
@click="$emit('close-modal')"
>
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
></div>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
>
<div
@click.stop
class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
>
<div class="bg-white">
<div
class="flex items-center justify-between border-b border-gray-100 p-2"
>
<div class="flex items-center">
<button
@click="$emit('close-modal')"
class="flex items-center justify-center fas fa-times text-primary text-lg p-2 h-10 w-10 hover:bg-blue-50"
></button>
<span class="font-bold text-lg">프로필 수정</span>
</div>
<div class="text-right mr-2">
<button
@click="onSaveProfile"
class="hover:bg-dark bg-primary text-white font-bold px-3 py-1 rounded-full"
>
저장
</button>
</div>
</div>
<!-- image section -->
<div class="h-60">
<!-- background image -->
<div
class="bg-gray-300 h-40 relative flex-none flex items-center justify-center"
>
<img
:src="`${currentUser.background_image_url}`"
ref="backgroundImage"
class="object-cover absolute h-full w-full"
/>
<button
@click="onChangeBackgroundImage"
class="absolute h-10 w-10 hover:text-gray-200 rounded-full fas fa-camera text-white text-lg"
></button>
<input
@change="previewBackgroundImage"
type="file"
accept="image/"
id="backgroundImageInput"
class="hidden"
/>
<!-- profile image -->
<img
:src="`${currentUser.profile_image_url}`"
ref="profileImage"
class="border-4 border-white w-28 h-28 absolute -bottom-14 left-2 rounded-full"
/>
<button
@click="onChangeProfileImage"
class="absolute h-10 w-10 -bottom-5 left-11 hover:text-gray-200 rounded-full fas fa-camera text-white text-lg"
></button>
<input
@change="previewProfileImage"
type="file"
accept="image/"
id="profileImageInput"
class="hidden"
/>
</div>
</div>
<div class="flex flex-col p-2">
<div
class="mx-2 my-1 px-2 py-1 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<div class="text-sm">이름</div>
<input
type="text"
:value="currentUser.email"
class="text-black focus:outline-none"
/>
</div>
<div
class="mx-2 my-1 px-2 py-5 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<input
type="text"
class="text-black focus:outline-none"
placeholder="자기소개"
/>
</div>
<div
class="mx-2 my-1 px-2 py-1 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<input
type="text"
class="text-black focus:outline-none"
placeholder="위치"
/>
</div>
<div
class="mx-2 my-1 px-2 py-3 text-gray border border-gray-200 rounded hover:border-primary hover:text-primary"
>
<input
type="text"
class="text-black focus:outline-none"
placeholder="웹사이트"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, computed } from "vue";
import addTweet from "../utils/addTweet";
import store from "../store";
import { storage, db } from "../firebase";
import { doc, updateDoc } from "firebase/firestore";
import {
getDownloadURL,
uploadString,
ref as storageRef,
uploadBytes,
} from "firebase/storage";
export default {
setup(props, { emit }) {
const tweetBody = ref("");
const currentUser = computed(() => store.state.user);
const backgroundImage = ref(null);
const backgroundImageData = ref(null);
const profileImageData = ref(null);
const profileImage = ref(null);
//트윗 추가 함수 -> utils의 addTweet함수
const onAddTweet = async () => {
try {
addTweet(tweetBody.value, currentUser.value);
tweetBody.value = "";
emit("close-modal"); //상위 컴포넌트에 close-modal실행
} catch (e) {
console.log("트윗 에러 메시지", e);
}
};
const onChangeBackgroundImage = () => {
document.getElementById("backgroundImageInput").click();
};
const onChangeProfileImage = () => {
document.getElementById("profileImageInput").click();
};
const previewBackgroundImage = (e) => {
const file = e.target.files[0];
let reader = new FileReader();
//이미지 선택되었을 때
backgroundImageData.value = file;
reader.onload = function (e) {
backgroundImage.value.src = e.target.result;
};
reader.readAsDataURL(file);
};
const previewProfileImage = (e) => {
const file = e.target.files[0];
let reader = new FileReader();
//이미지 선택되었을 때
profileImageData.value = file;
reader.onload = function (e) {
profileImage.value.src = e.target.result;
};
reader.readAsDataURL(file);
};
const onSaveProfile = async () => {
if (!profileImageData.value && !backgroundImageData.value) {
return;
}
try {
if (profileImageData.value) {
const profileStorageRef = storageRef(
storage,
`profile/${currentUser.value.uid}/profile`
);
const profileSnapshot = await uploadBytes(
profileStorageRef,
profileImageData.value
);
const profileImageUrl = await getDownloadURL(profileSnapshot.ref);
const userDocRef = doc(db, "users", currentUser.value.uid);
await updateDoc(userDocRef, { profile_image_url: profileImageUrl });
store.commit("SET_PROFILE_IMAGE", profileImageUrl);
}
} catch (e) {
console.error("Error uploading profile images:", error);
}
try {
if (backgroundImageData.value) {
const backgroundStorageRef = storageRef(
storage,
`profile/${currentUser.value.uid}/background`
);
const backgroundSnapshot = await uploadBytes(
backgroundStorageRef,
backgroundImageData.value
);
const backgroundImageUrl = await getDownloadURL(
backgroundSnapshot.ref
);
const userDocRef = doc(db, "users", currentUser.value.uid);
await updateDoc(userDocRef, {
background_image_url: backgroundImageUrl,
});
store.commit("SET_BACKGROUND_IMAGE", backgroundImageUrl);
}
} catch (e) {
console.error("Error uploading profile images:", error);
}
emit("close-modal");
};
return {
tweetBody,
onAddTweet,
currentUser,
onChangeBackgroundImage,
onChangeProfileImage,
previewBackgroundImage,
backgroundImage,
profileImage,
previewProfileImage,
onSaveProfile,
backgroundImageData,
profileImageData,
};
},
};
</script>