๐ TaskTracker - ํ๊ณ
๐ฏ ํ๋ก์ ํธ ๊ฐ์ ๋ณธ ํ๋ก์ ํธ๊ฐ ์งํฅํ๋ ๋ฐ๋ ๋ค์๊ณผ ๊ฐ์๋ค. ํ๋ก์ ํธ์ ํ์์ฑ์ ๋ณธ์ธ ์ค์ค๋ก ๋ฉ๋ํ ์ ์์ ๊ฒ Firebase๋ฅผ ํตํ Authentication๊ณผ CRUD์ ์ ๊ณผ์ ์ ๊ฒฝํํ ๊ฒ Redux-Toolkit์ ํตํด ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฒฝํํ ๊ฒ >
https://tasktracker-livid.vercel.app/home
๐ฏ ํ๋ก์ ํธ ๊ฐ์
๋ณธ ํ๋ก์ ํธ๊ฐ ์งํฅํ๋ ๋ฐ๋ ๋ค์๊ณผ ๊ฐ์๋ค.
1. ํ๋ก์ ํธ์ ํ์์ฑ์ ๋ณธ์ธ ์ค์ค๋ก ๋ฉ๋ํ ์ ์์ ๊ฒ 2. Firebase๋ฅผ ํตํ Authentication๊ณผ CRUD์ ์ ๊ณผ์ ์ ๊ฒฝํํ ๊ฒ 3. Redux-Toolkit์ ํตํด ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฒฝํํ ๊ฒ
โ ํ๋ก์ ํธ์ ํ์์ฑ
๋ถํธํ๋ค. ๊ณํ์ ์ธ์ฐ๋ฉด ์งํค์ง ์๊ณ , ์ธ์ฐ์ง ์์ผ๋ฉด ๋ถ์ํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ชจ๋ ๊ฒ์ ์์ธกํ ๋งํผ ๋๋ํ์ง ์์๋ค. ๋งค๋ฒ ์ํํ๋ ค๋ ์์ ์ ๋์ด๋๋ ์์ดํ๊ณ , ์์ธกํ ์ ์๋ ๋ฌด์ธ๊ฐ๋ฅผ ์์ธกํ๋ ค ํ๊ธฐ์ ๋น์ฐํ ์คํจํ๋ฉด์, ๋ฌด์์ธ๊ฐ ์๋ชป๋๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
์์ธ๊ณผ ๊ฒฐ๊ณผ๊ฐ ๋ฐ๋๋ ์๊ฐ์ด ๋ถ๋ช ํ ์๋ค. ๊ทธ๋ฌ๋๊น, ๊ณํ๊ณผ ์คํ์ ๋ฌธ์ ๊ฐ ์์ด์ ๋ถ์ํด์ง๋ ๊ฒ์ด ์๋๋ผ, ๋ถ์ํ๊ธฐ ๋๋ฌธ์ ๊ณํ๊ณผ ์คํ์ด ๋ง๊ฐ์ง๋ ์ํฉ์ด ์๋ค๋ ๊ฒ์ด๋ค. ๋ถ์์ด ์์ธ์ด๋ผ๋ฉด ๊ทธ๊ฒ์ ๊ฒฝ๊ฐํ๋ ค๋ ์ชฝ์ด ํด๊ฒฐ์ ๊ฐ๊น๊ฒ ๋ค.
๋ฝ๋ชจ๋๋ก ๊ธฐ๋ฒ์ ๊ฝค๋ ์ ๋ช ํ ์๊ฐ๊ด๋ฆฌ ๋ฐฉ๋ฒ๋ก ์ค ํ๋์๊ณ , ๋ฒํผ์ ํด๋ฆญํ๋ ์๊ฐ ๋ด๊ฐ ์ค์ ํ Task์ ์๊ฐ์ ์จ์ ํ ์ง์คํ๊ฒ ๋๋ค๋ ์ ์ด ๋ถ์ ๊ด๋ฆฌ์ ์๋นํ ์ ์ฉํ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
ํ๋ก์ ํธ๋ ์ฌ์ค์ ๊ป๋ฐ๊ธฐ์ ๋ถ๊ณผํ๋ค. ํ๋ก์ ํธ๋ผ๋ ๋ฐ์ด๋๋ฆฌ ์์์ ์ด๋ ํ ๊ธฐ์ ์ ์ต๋ํ๊ณ ์๋ จํ์ผ๋ฉฐ ๋ฌด์จ ์ด์ผ๊ธฐ๋ฅผ ๋ด์๋์ง๊ฐ ํจ์ฌ ์ค์ํ๊ณ , ์ด๋ฌํ ์์ ์ ์ฌ์ ํ ์ด๋ ต์ง๋ง, ๊ณ ๋ฏผ์ ์ด๋์ด ํ๋ก์ ํธ์ด๊ธฐ ๋๋ฌธ์ ๊ณ ๋ฏผ์ด ์ ์ ๋์ง ์์ ํ๋ก์ ํธ๋ฅผ ์ํํ ๊ฒ์ด๋ผ๋ฉด, ๊ทธ๋ฅ ๋๊ฐ์ ๋ ธ๋ ๊ฒ ํ๋ณต์ ๋ ์ ๋ฆฌํ ๊ฒ์ด๋ผ๋ ๊ฒ์ด ๋ด ์๊ฐ์ด์๋ค. ์ญ์ค์ ์ด๋ค.
โ Authentication
1. createUserWithEmailAndPassword(ํ์ ๊ฐ์ ) โ๏ธ
// ํ์๊ฐ์
์ ์ฒ๋ฆฌํ๋ ํจ์
const handleSignUp = async (e) => {
e.preventDefault();
if (Object.values(errors).some((error) => error)) {
alert("Please check your information again.");
return;
}
await dispatch(
signupUser({
email: userInfo?.email,
password: userInfo?.password,
imgFile,
displayName: userInfo?.username,
})
).unwrap();
alert("Sign up successful!");
navigate("/signin");
};export const signupUser = createAsyncThunk(
"user/signupUser",
async (userInfo) => {
try {
// Firebase๋ฅผ ํตํด, ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ก ์ฌ์ฉ์๋ฅผ ์์ฑ
await createUserWithEmailAndPassword(
auth,
userInfo?.email,
userInfo?.password
);
// ํ์ฌ ์ฌ์ฉ์๊ฐ ์๋ ๊ฒฝ์ฐ(=firebase์ ํ์ ๋ฑ๋ก์ด ๋ ๊ฒฝ์ฐ)
if (auth.currentUser) {
// ๋์์ ์ด๋ฏธ์ง ํ์ผ์ด ์๋ ๊ฒฝ์ฐ
if (userInfo?.imgFile) {
// firebase์ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋
const storageRef = ref(
storage,
new Date().getTime() + userInfo?.imgFile?.name
);
const uploadTask = uploadBytesResumable(
storageRef,
userInfo?.imgFile
);
uploadTask.on(
"state_changed",
// ์
๋ก๋ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋ ์คํ๋ ์ฝ๋ฐฑ ํจ์
(snapshot) => {
// ์
๋ก๋ ์งํ๋ฅ ์ ๊ณ์ฐํ๋ ๋ก์ง
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log("Upload is " + progress + "% done");
switch (snapshot.state) {
case "paused":
console.log("Upload is paused");
break;
case "running":
console.log("Upload is running");
break;
}
},
// ์
๋ก๋ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
(error) => {
alert("Failed to upload image. Please try again later.");
},
// ์
๋ก๋๊ฐ ์๋ฃ๋ ๊ฒฝ์ฐ
() => {
// ์
๋ก๋๋ ํ์ผ์ ๋ค์ด๋ก๋ URL์ ๊ฐ์ ธ์ด
getDownloadURL(uploadTask.snapshot.ref).then(
async (downloadURL) => {
// ์ฌ์ฉ์ ํ๋กํ์ ์
๋ฐ์ดํธ
await updateProfile(auth.currentUser, {
displayName: userInfo?.displayName,
photoURL: downloadURL,
});
}
);
}
);
}
// ์ด๋ฏธ์ง ํ์ผ์ด ์๋ ๊ฒฝ์ฐ
else {
await updateProfile(auth.currentUser, {
displayName: userInfo?.displayName,
photoURL: "",
});
}
}
} catch (error) {
// ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
switch (error.code) {
case "auth/email-already-in-use":
alert("Email already in use.");
break;
case "auth/weak-password":
alert("Password should be at least 6 characters.");
break;
case "auth/network-request-failed":
alert("Network request failed.");
break;
case "auth/invalid-email":
alert("Invalid email format.");
break;
case "auth/internal-error":
alert("Internal error.");
break;
default:
alert("Sign up failed.");
}
}
}
);์ฝ๋๋ฅผ ์ฃผ์ ์ฃผ์ ์ค๋ช ํ๋ ๊ฒ์ ํฌ๊ฒ ์๋ฏธ๊ฐ ์์ ๊ฒ ๊ฐ๋ค. Firebase์์ ์ ๊ณตํ๋ createUserWithEmailAndPassword ํจ์์ ์ ์ ํ ๊ฐ์ ์ ๋ฌํ๋ฉด ํ์ ๊ฐ์ ์ ์๋ฃ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ๋ฃ๊ฐ ๋ฌด์์ธ์ง ๊ทธ ์์ฑ์ด ์ด๋ ํ์ง๋ ๋ช ํํ ์ธ์งํด์ผ ํ๋ค. ํฅํ์ React-Query๋ Supabase ๋ฑ ๋ค์ํ ๊ธฐ์ ์ ํ๋ก์ ํธ์ ์ ์ฉํ ์์ ์ธ๋ฐ, ๋ด๊ฐ ํ์ฉํ ์ ์๋ ๋ฐ์ดํฐ์๋ ๋ฌด์์ด ์์ผ๋ฉฐ, ๊ทธ ๋ฐ์ดํฐ๋ค์ด ๊ฐ์ง๋ ์์ฑ์ด ๋ฌด์์ธ์ง ์์ง ๋ชปํ๋ฉด, ์ ๋๋ก ๋ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
๋ผ์ง๊ณ ๊ธฐ๊ฐ ์ํ๋๋ฉด ํฉํ์ด๋๋ฅผ ๊ฑฐ์ณ ์๋ฏธ๋ ธ์ฐ์ผ๋ก ๋ฐ๋๋๋ฐ, ์ด๋ ํ์ํ ๊ฒ์ด ๋จ๋ฐฑ์ง ๋ถํด ํจ์์ธ 'ํ๋กํ ์์ '๊ณ , ์์ฐ์ ์ด ๋ฐํจ๋๋ ๋์ ์์ฑ๋๋ ํ๋กํ ์์ ๋ ์ผ์ข ์ ์ํ์ ์ญํ ์ ํ๊ธฐ์, ์ผ๋ฐ์ ์ผ๋ก ์๋๊ตญ์ ๊ฐ์ ์์ฐ์ ์ผ๋ก ํ๋ค.
์ฝ๋ฉ์ ์ง์์ด ์๋์ ์ผ๋ก ๋ง์, ๊น๋ค๋ก์ด ์๋(like ๋ฐฑ์ข ์ ์ ์๋)์๊ฒ ์์ฌ๋ฅผ ๋์ ํ๋ ๊ณผ์ ๊ณผ ๊ฐ์์, ๋ด๊ฐ ์ฌ์ฉํ๋ ์ฌ๋ฃ๋ฅผ ๋ช ํํ ์๊ณ ์์ด์ผ๋ง ํ๋ค.
2. signInWithEmailAndPassword(๋ก๊ทธ์ธ) โ๏ธ
// ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํ๋ ํจ์
const handleSignIn = async () => {
try {
await dispatch(
signInUser({ email: signinInfo?.email, password: signinInfo?.password })
).unwrap();
navigate("/home");
} catch (error) {
console.error(error);
}
};export const signInUser = createAsyncThunk(
"user/signInUser",
async (userInfo) => {
const userCredential = await signInWithEmailAndPassword(
auth,
userInfo?.email,
userInfo?.password
);
const result = {
uid: userCredential?.user?.uid,
displayName: userCredential?.user?.displayName,
email: userCredential?.user?.email,
};
localStorage.setItem("user", JSON.stringify(result));
return result;
}
);Firebase์์ ์ ๊ณตํ๋ ํจ์๋ฅผ ์ฌ์ฉํ๋ค๋ ์ ์ ์ ์ธํ๊ณ ํน์ง์ ์ธ ๊ฒ์, ์ ์ ์ ๋ณด๋ฅผ localstorage์ ์ ์ฅํ๋ค๋ ์ ์ด๋ค.
์ฌ๋ฌ ์ฅ๋จ์ ์ด ์๊ฒ ์ง๋ง, ๋ธ๋ผ์ฐ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ฉด, ์ ์ ๊ฐ ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์๋ค๊ฐ ๋ค์ ์ด์ด๋, localstorage์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ ๊ทธ๋๋ก ์ ์ง๋๊ธฐ์, ๋ก๊ทธ์ธ ์ํ ์ ์ง ๋ฑ ์ธ์ ๊ด๋ จ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋ฐ ์ ์ฉํ๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ๋ค.
3. onAuthStateChanged(ํ์ ์ ์ง) โ๏ธ
useEffect(() => {
dispatch(checkLoginUser(JSON.parse(localStorage.getItem("user"))));
}, [dispatch]);// ์ ์ ์ ๋ณด๋ฅผ ์ค์ ํ๋ ๋น๋๊ธฐ ํจ์
export const checkLoginUser = createAsyncThunk(
"user/checkLoginUser",
async (user) => {
console.log(user);
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
console.log(user);
localStorage.setItem("user", JSON.stringify(user));
});
return user;
}
);
Appbar์์๋ ํด๋น useEffect๋ฅผ ์คํํ๋ค. ์ฆ, Appbar๊ฐ ์ ์๋๋ ๋ชจ๋ ์์ญ์์ ๊ณ์ํด์ ์ ์ ์ ๋ณด๋ฅผ checkLoginUser ํจ์๋ก dispatch ํ๋ค๋ ๊ฒ์ด๋ค.
Firebase์์ ์ ๊ณตํ๋ onAuthStateChanged ํจ์๋ฅผ ํตํด ๊ณ์ํด์ ์ ์ ์ ๋ณด๋ฅผ ๊ฐฑ์ ๋ฐ ์ ์งํ ์ ์๊ฒ ๋๋ค.
โ CRUD
1. Add Task ๋ฒํผ โ๏ธ
๋์: ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ์๋ก์ด ํ์คํฌ ์ด๋ฆ์ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๋ค. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ "์ด๋ํ๊ธฐ"๋ผ๋ ํ์คํฌ๋ฅผ ์ ๋ ฅํ๋ฉด, ์ด ํ์คํฌ๊ฐ ๋ฆฌ์คํธ์ ์ถ๊ฐ๋๋ค.
2. Go to Archive ๋ฒํผ โ๏ธ
๋์: ํ์ฌ ํ๋ฉด์์ ์์นด์ด๋ธ ํ์ด์ง๋ก ์ด๋ํ๋ค. ์ด๋ ํ์๋ ํ ํ์ด์ง์ ์๋ ๋ชจ๋ complete ํ์คํฌ๋ค์ ํ์ธํ ์ ์๋ค.
3. All Clear ๋ฒํผ โ๏ธ
๋์: ์์นด์ด๋ธ ํ์ด์ง์ ์๋ ๋ชจ๋ complete ํ์คํฌ๋ค์ ์ญ์ ํ๋ค. ๋จ, ํ ํ์ด์ง์ ์ถ๊ฐ๋ ๋ด์ฉ์ complete๊ฐ ์๋๊ธฐ์ ์ญ์ ๋์ง ์๋๋ค.
4. Go Home ๋ฒํผ โ๏ธ
๋์: ์์นด์ด๋ธ ํ์ด์ง์์ ํ ํ์ด์ง๋ก ๋๋์๊ฐ๋ค. ์ด๋ ํ์๋ ๋ค์ ํ ํ์ด์ง์ ์ถ๊ฐ๋์ด ์๋ ํ์คํฌ๋ค๊ณผ ์์นด์ด๋ธ ๋ฒํผ์ ํ์ธํ ์ ์๋ค.
5. Start ๋ฒํผ โ๏ธ
๋์: ์ ํํ ํน์ ํ์คํฌ์ ํ์ด๋จธ๋ฅผ ์์ํ๋ค. ์๋ฅผ ๋ค์ด, "์ด๋ํ๊ธฐ" ํ์คํฌ์ ํ์ด๋จธ๋ฅผ ์์ํ๋ฉด, ํด๋น ํ์คํฌ๋ 'pending' ์ํ๋ก ํ์๋๋ฉฐ ์๊ฐ์ด ์งํ๋๋ค.
6. Pause ๋ฒํผ โ๏ธ
๋์: ํ์ฌ ์งํ ์ค์ธ ํ์คํฌ์ ํ์ด๋จธ๋ฅผ ์ผ์ ์ ์งํ๋ค. ์๋ฅผ ๋ค์ด, "์ด๋ํ๊ธฐ" ํ์คํฌ์ ํ์ด๋จธ๊ฐ ์ผ์ ์ ์ง๋๋ฉด, ํด๋น ํ์คํฌ๋ 'paused' ์ํ๋ก ๋ณ๊ฒฝ๋๋ฉฐ ์ ์ง๋ ๊ธฐ๋ก์ผ๋ก ํด๋น ํ์คํฌ๊ฐ ์๋ฒ์ ์ ์ฅ๋๋ค.
7. Delete ๋ฒํผ โ๏ธ
๋์: ์ ํํ ํน์ ํ์คํฌ๋ฅผ ๋ฆฌ์คํธ์์ ์ญ์ ํ๋ค. ์๋ฅผ ๋ค์ด, "์ด๋ํ๊ธฐ" ํ์คํฌ๋ฅผ ์ญ์ ํ๋ฉด, ์ด ํ์คํฌ๋ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ๋์ด UI์์ ๋ณด์ด์ง ์๊ฒ ๋๋ค.
8. ๋ํ ์ฝ๋ โ๏ธ
const handleAddTask = () => {
if (!task.trim()) {
return alert("ํ์คํฌ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.");
}
const newTask = {
createdAt: Date.now(),
task,
status: "pending",
initialTime: time,
pausedTime: "",
userId: user.uid,
};
dispatch(addTask(newTask));
setTask("");
setTime(300);
};import { createSlice } from "@reduxjs/toolkit";
import { createAsyncThunk } from "@reduxjs/toolkit";
import {
addDoc,
getDocs,
deleteDoc,
doc,
collection,
updateDoc,
} from "firebase/firestore";
import { db } from "../firebase";
// ๋ชจ๋ ํ์คํฌ ๊ฐ์ ธ์ค๊ธฐ
export const getAllTasks = createAsyncThunk(
"tasks/getAllTasks",
async (userId) => {
const taskCollection = await getDocs(collection(db, "tasks"));
const allTasks = [
...taskCollection.docs.map((doc) => ({ ...doc.data(), id: doc.id })),
].filter((task) => task.userId === userId);
return allTasks;
}
);
// ํ์คํฌ ์ถ๊ฐํ๊ธฐ
export const addTask = createAsyncThunk("tasks/addTask", async (data) => {
const result = await addDoc(collection(db, "tasks"), {
userId: data?.userId,
task: data?.task,
initialTime: data?.initialTime,
pausedTime: "",
createdAt: data?.createdAt,
status: data.status,
});
const taskId = result.path.split("/")[1];
return { ...data, id: taskId };
});
// ํ์คํฌ ์ญ์ ํ๊ธฐ
export const deleteTask = createAsyncThunk(
"tasks/deleteTask",
async (taskId) => {
console.log(taskId);
await deleteDoc(doc(db, "tasks", taskId));
return taskId;
}
);
// ๋ฉ์ถ ํ์คํฌ ์ถ๊ฐํ๊ธฐ
export const putTask = createAsyncThunk("tasks/putTask", async (task) => {
if (task.pausedTime !== "") {
await updateDoc(doc(db, "tasks", task.id), {
pausedTime: task?.pausedTime,
status: task.status,
});
}
return task;
});
// ์๋ฃ๋ ํ์คํฌ ์ถ๊ฐํ๊ธฐ
export const completedTask = createAsyncThunk(
"tasks/completedTask",
async (task) => {
if (task?.status === "complete") {
await updateDoc(doc(db, "tasks", task.id), {
status: task?.status,
});
}
return task;
}
);
// ๋ชจ๋ 'complete task' ์ญ์ ํ๊ธฐ
export const allClear = createAsyncThunk(
"tasks/allClearTask",
async (userId) => {
const taskCollection = await getDocs(collection(db, "tasks"));
const tasksToDelete = taskCollection.docs.filter(
(doc) => doc.data().userId === userId && doc.data().status === "complete"
);
const deletePromises = tasksToDelete.map((task) =>
deleteDoc(doc(db, "tasks", task.id))
);
await Promise.all(deletePromises);
return tasksToDelete.map((task) => task.id);
}
);
const initialState = {
tasks: [],
loading: false,
error: null,
};
const tasksSlice = createSlice({
name: "tasks",
initialState,
extraReducers: (builder) => {
builder.addCase(getAllTasks.pending, (state) => {
state.loading = true;
});
builder.addCase(getAllTasks.fulfilled, (state, action) => {
state.loading = false;
state.tasks = action.payload;
});
builder.addCase(getAllTasks.rejected, (state) => {
state.loading = false;
});
builder.addCase(addTask.pending, (state) => {
state.loading = true;
});
builder.addCase(addTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks.push(action.payload);
});
builder.addCase(addTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(deleteTask.pending, (state) => {
state.loading = true;
});
builder.addCase(deleteTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.filter((task) => task.id !== action.payload);
});
builder.addCase(deleteTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(putTask.pending, (state) => {
state.loading = true;
});
builder.addCase(putTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.map((task) =>
task.id === action.payload.id ? action.payload : task
);
});
builder.addCase(putTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(completedTask.pending, (state) => {
state.loading = true;
});
builder.addCase(completedTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
window.alert("Task saved successfully ๐");
});
builder.addCase(completedTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(allClear.pending, (state) => {
state.loading = true;
});
builder.addCase(allClear.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.filter((task) => !action.payload.some(task.id));
});
builder.addCase(allClear.rejected, (state) => {
state.loading = false;
});
},
});
export default tasksSlice.reducer;
๋์๊ฒ ํ์ํ ๋ฐ์ดํฐ์ ํ๋กํผํฐ๋ฅผ ๋ช ํํ ํจ์ผ๋ก์จ, db์ ๋ถ์ฐ์ ์ค์ฌ, ํจ์จ์ ์ธ ์ํ ๊ด๋ฆฌ๋ฅผ ์ํํ๊ฒ ๋ค๋ ๊ฒ์ด ํต์ฌ์ด๋ค.
โ ์์ธ์ฒ๋ฆฌ
1. ํ์ ๊ฐ์ ์ ํ์ํ ์์ธ์ฒ๋ฆฌ๋ฅผ util๋ก ๋ถ๋ฆฌ โ๏ธ
export const validateUsername = (username) => {
const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/;
return usernameRegex.test(username)
? ""
: "3์ ์ด์ 16์ ์ดํ, ์๋ฌธ์, ์ซ์, ๋ฐ์ค(_)๋ง ํ์ฉ๋ฉ๋๋ค.";
};
export const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email) ? "" : "์๋ชป๋ ์ด๋ฉ์ผ ํ์์
๋๋ค.";
};
export const validatePassword = (password) => {
const passwordRegex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return passwordRegex.test(password)
? ""
: "์ต์ 8์ ์ด์, ๋๋ฌธ์, ์๋ฌธ์, ์ซ์, ํน์๋ฌธ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.";
};
2. ๋ก๊ทธ์ธ ์ฌ๋ถ์ ๋ฐ๋ฅธ ๋ผ์ฐํ ์์ธ ์ฒ๋ฆฌ โ๏ธ
import { useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
const usePreventAuth = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
useEffect(() => {
if (JSON.parse(localStorage.getItem("user"))) {
navigate("/home");
if (pathname === "/archive") {
navigate("/archive");
}
} else {
if (pathname === "/home" || pathname === "/archive") {
navigate("/");
}
}
}, []);
return;
};
export default usePreventAuth;3. loading์ ํตํ, (API ํต์ ์ ) Button disabled ์ฒ๋ฆฌ โ๏ธ
<Button disabled={loading} onClick={handleSignIn}>
Sign In
</Button>const Button = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: 300px;
padding: 1rem 0;
margin: 1rem;
font-size: 1.5rem;
color: black;
background: white;
border: 2px solid white;
border-radius: 50px;
box-shadow: 0px 4px 15px rgba(255, 255, 255, 0.2);
transition: all 0.3s ease-in-out;
cursor: pointer;
opacity: 0;
animation: fadeIn 1.5s ease forwards;
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
}
}
&:hover {
background: #ffffff;
color: black;
transform: translateY(-5px);
box-shadow: 0px 8px 20px rgba(255, 255, 255, 0.3);
}
&:active {
transform: translateY(0);
box-shadow: 0px 4px 15px rgba(255, 255, 255, 0.2);
}
&:disabled {
background-color: gray;
}
`;4. .env๋ฅผ ํตํ ํ๊ฒฝ๋ณ์ ์ฒ๋ฆฌ โ๏ธ
import { initializeApp } from "firebase/app";
import { getAuth, onAuthStateChanged } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: "task-tracker3.firebaseapp.com",
projectId: "task-tracker3",
storageBucket: "task-tracker3.appspot.com",
messagingSenderId: "977756658453",
appId: "1:977756658453:web:588402c61ac23db27c671c",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const storage = getStorage(app); // Initialize storage
export { auth, db, storage, onAuthStateChanged };โ ํ๊ณ
์ค๋ฌด ์ด์ด ๊ฐ ๋์์ ๋ ์ ๊น ํ๋ฆฌ๋ฐ๊ฒ๋จ์์ ์๋ฅด๋ฐ์ดํธ๋ฅผ ํ ์ ์ด ์๋ค. ํ ํ ๋จธ๋๊ฐ ๋ฐ๋ฏํ ๋ฌผ์ ์ค ์ ์๋๊ณ ์ฌ์ญค๋ณด์ ์ ๋จ๊ฑฐ์ด ๋ฌผ์ ์ฐฌ๋ฌผ์ ์ข ํ๊ณ ์์๋๋ฐ, ์ค์ฅ๋์ด ์ฐฌ๋ฌผ์ ์ ๋ถ๋๊ณ ํธํต ์๋ ํธํต์ ์ณค๋ค. ๊ทธ๋ฐ๋ฐ ํ ๋จธ๋์ ์์ผ์ จ๋ค. '๋จ๊ฑฐ์ด' ๋ฌผ์ด ์๋๋ผ, '๋ฐ๋ฏํ' ๋ฌผ์ ๋ฌ๋ผ๊ณ ํ์ผ๋ ๋ด๊ฐ ๋ง๋ค๊ณ ๋ง์ํ์ จ๋ค. ๋ฌผ๋ก ์ค์ฅ๋๋ ์ฌ๊ณผํ์ จ๋ค.
๊ฐ๋์ ๋ํ ์ผ์์ ์จ๋ค. ์ฌ๋ฐ๋ ์ ์, ๊ทธ ๋ํ ์ผ์ด๋ผ๋ ๊ฒ์ ๋๋จํ ํ๋จ์ด ์๋๋ผ ์ธ์ฌํ ๊ด์ฐฐ๋ก๋ถํฐ ์ป์ ์ ์๋ค. ๋จธ๋ฆฌํธ ๋น ์ง ๋๊น์ง ์ณ๋ค๋ณธ ํ๋ก๋ํธ๋ ํ๋ฆฌํฐ๊ฐ ์ข์ ์๋ฐ์ ์๊ณ , ์๋ง๋ ์ฌ์ฉ์์๊ฒ ๊ฐ๋์ผ๋ก ๋ค๊ฐ๊ฐ ๊ฒ์ด๋ค. ๋ฌผ๋ก ๊ทธ๋ ์ง ์๋๋ผ๋ ๋น์ฐํ ๋ง์ด ๊ด์ฐฐํด์ผ ๋๋ค. ๊ธฐ๋ณธ์ด๋๊น. '๋ถ์ง๋ฐํจ'์ ๋ํ ๊ฒฝ๊ฐ์ฌ์ด ์ด๋ฒ ํ๋ก์ ํธ์ ์ต๋ ๊ตํ ์๋์์๊น.
More to read
AI&ML ๊ธฐ์ด
Reference: https://bettermesol.github.io/ml/2019/09/16/ai-ml-dl/AI: ๊ธฐ๊ณ๊ฐ ์ฌ๋์ฒ๋ผ ์๊ฐํ๊ณ ํ๋จํ๊ฒ ๋ง๋๋ ๊ฐ์ฅ ๋์ ๋ฒ์ฃผ์ ๊ธฐ์ ์ ๋๋ค.ML: ๋ฐ์ดํฐ๋ฅผ ํ์ตํ์ฌ ์ค์ค๋ก ๊ท์น์ ์ฐพ์๋ด๋ AI์ ํ ๋ถ์ผ๋ก,
'AI Agent Economy'Novitas : AI Agent๊ฐ ์ง๊ฐ์ ๊ฐ์ง๋ ์ธ์
์ผ๋ง ์ , ๋ฏธ๋์์ ์ฆ๊ถ ๋ฆฌ์์น ๋ฆฌํฌํธ(์ฌํด๋ ์ด๋๋ฆฌ์์ด๋ค: ์์ด์ ํธ ์๋์ Near Automata)๋ฅผ ์ ํ๊ฒ ๋์์ต๋๋ค. AI Agent๋ฅผ ์ธ๊ฐ๊ณผ ํจ๊ปํ ๊ฒฝ์ ์ฃผ์ฒด๋ก ๋ฐ๋ผ๋ณด๋ ์๊ฐ์ ์ ์์ด ์ถฉ๊ฒฉ์ ๋ฐ์๋๋ฌ์ฃ .ํ ๊ฐ์ง ์ง๊ณ ๋์ด๊ฐ ๋ถ๋ถ์ด ์์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ํํ 'AI'
'ERC-8004'Novitas: AI ์์ด์ ํธ ๊ฒฝ์ ์ฃผ์ฒด
Web 4.0์ ํ ๋ฌธ์ฅ์ผ๋ก ์ ์ํ๋ฉด Sovereign Transact์ ๋๋ค.AI๊ฐ ์ธ๊ฐ์ ํ๋ฝ ์์ด ์ง๊ฐ์ ์์ ํ๊ณ , ๊ฒฐ์ ๋ฅผ ์ํํ๋ฉฐ, ์ธํ๋ผ๋ฅผ ํต์ ํ๋ ์ฃผ๊ถ์ ๊ฒฝ์ ์ฃผ์ฒด๊ฐ ๋๋ ์ธ๊ณ์ ๋๋ค. Web 3.0์ด ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ์ ํ์ค์ํ๋ฅผ ์คํํ๋ค๋ฉด, Web 4.0์ ๊ทธ