'TaskTracker'

๐Ÿ… TaskTracker - ํšŒ๊ณ 

๐ŸŽฏ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ณธ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ง€ํ–ฅํ•˜๋Š” ๋ฐ”๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•˜๋‹ค. ํ”„๋กœ์ ํŠธ์˜ ํ•„์š”์„ฑ์„ ๋ณธ์ธ ์Šค์Šค๋กœ ๋‚ฉ๋“ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ Firebase๋ฅผ ํ†ตํ•œ Authentication๊ณผ CRUD์˜ ์ „ ๊ณผ์ •์„ ๊ฒฝํ—˜ํ•  ๊ฒƒ Redux-Toolkit์„ ํ†ตํ•ด ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ฒฝํ—˜ํ•  ๊ฒƒ >

2024๋…„ 6์›” 22์ผ10min read

https://tasktracker-livid.vercel.app/home

๐ŸŽฏ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

๋ณธ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ง€ํ–ฅํ•˜๋Š” ๋ฐ”๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•˜๋‹ค.

1. ํ”„๋กœ์ ํŠธ์˜ ํ•„์š”์„ฑ์„ ๋ณธ์ธ ์Šค์Šค๋กœ ๋‚ฉ๋“ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ 2. Firebase๋ฅผ ํ†ตํ•œ Authentication๊ณผ CRUD์˜ ์ „ ๊ณผ์ •์„ ๊ฒฝํ—˜ํ•  ๊ฒƒ 3. Redux-Toolkit์„ ํ†ตํ•ด ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ฒฝํ—˜ํ•  ๊ฒƒ

โœ… ํ”„๋กœ์ ํŠธ์˜ ํ•„์š”์„ฑ

๋ถˆํŽธํ–ˆ๋‹ค. ๊ณ„ํš์„ ์„ธ์šฐ๋ฉด ์ง€ํ‚ค์ง€ ์•Š๊ณ , ์„ธ์šฐ์ง€ ์•Š์œผ๋ฉด ๋ถˆ์•ˆํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ชจ๋“  ๊ฒƒ์„ ์˜ˆ์ธกํ•  ๋งŒํผ ๋˜‘๋˜‘ํ•˜์ง€ ์•Š์•˜๋‹ค. ๋งค๋ฒˆ ์ˆ˜ํ–‰ํ•˜๋ ค๋Š” ์ž‘์—…์˜ ๋‚œ์ด๋„๋Š” ์ƒ์ดํ–ˆ๊ณ , ์˜ˆ์ธกํ•  ์ˆ˜ ์—†๋Š” ๋ฌด์–ธ๊ฐ€๋ฅผ ์˜ˆ์ธกํ•˜๋ ค ํ–ˆ๊ธฐ์— ๋‹น์—ฐํžˆ ์‹คํŒจํ•˜๋ฉด์„œ, ๋ฌด์—‡์ธ๊ฐ€ ์ž˜๋ชป๋๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

์›์ธ๊ณผ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ”๋€Œ๋Š” ์ˆœ๊ฐ„์ด ๋ถ„๋ช…ํžˆ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ, ๊ณ„ํš๊ณผ ์‹คํ–‰์— ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด์„œ ๋ถˆ์•ˆํ•ด์ง€๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ๋ถˆ์•ˆํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ณ„ํš๊ณผ ์‹คํ–‰์ด ๋ง๊ฐ€์ง€๋Š” ์ƒํ™ฉ์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋ถˆ์•ˆ์ด ์›์ธ์ด๋ผ๋ฉด ๊ทธ๊ฒƒ์„ ๊ฒฝ๊ฐํ•˜๋ ค๋Š” ์ชฝ์ด ํ•ด๊ฒฐ์— ๊ฐ€๊น๊ฒ ๋‹ค.

๋ฝ€๋ชจ๋„๋กœ ๊ธฐ๋ฒ•์€ ๊ฝค๋‚˜ ์œ ๋ช…ํ•œ ์‹œ๊ฐ„๊ด€๋ฆฌ ๋ฐฉ๋ฒ•๋ก  ์ค‘ ํ•˜๋‚˜์˜€๊ณ , ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋Š” ์ˆœ๊ฐ„ ๋‚ด๊ฐ€ ์„ค์ •ํ•œ Task์™€ ์‹œ๊ฐ„์— ์˜จ์ „ํžˆ ์ง‘์ค‘ํ•˜๊ฒŒ ๋œ๋‹ค๋Š” ์ ์ด ๋ถˆ์•ˆ ๊ด€๋ฆฌ์— ์ƒ๋‹นํžˆ ์œ ์šฉํ•˜๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ๋Š” ์‚ฌ์‹ค์ƒ ๊ป๋ฐ๊ธฐ์— ๋ถˆ๊ณผํ•˜๋‹ค. ํ”„๋กœ์ ํŠธ๋ผ๋Š” ๋ฐ”์šด๋”๋ฆฌ ์•ˆ์—์„œ ์–ด๋– ํ•œ ๊ธฐ์ˆ ์„ ์Šต๋“ํ•˜๊ณ  ์ˆ™๋ จํ–ˆ์œผ๋ฉฐ ๋ฌด์Šจ ์ด์•ผ๊ธฐ๋ฅผ ๋‹ด์•˜๋Š”์ง€๊ฐ€ ํ›จ์”ฌ ์ค‘์š”ํ•˜๊ณ , ์ด๋Ÿฌํ•œ ์ž‘์—…์€ ์—ฌ์ „ํžˆ ์–ด๋ ต์ง€๋งŒ, ๊ณ ๋ฏผ์˜ ์ด๋Ÿ‰์ด ํ”„๋กœ์ ํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ณ ๋ฏผ์ด ์ „์ œ๋˜์ง€ ์•Š์€ ํ”„๋กœ์ ํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์ด๋ผ๋ฉด, ๊ทธ๋ƒฅ ๋‚˜๊ฐ€์„œ ๋…ธ๋Š” ๊ฒŒ ํ–‰๋ณต์— ๋” ์œ ๋ฆฌํ•  ๊ฒƒ์ด๋ผ๋Š” ๊ฒƒ์ด ๋‚ด ์ƒ๊ฐ์ด์—ˆ๋‹ค. ์—ญ์„ค์ ์ด๋‹ค.

โœ… Authentication

1. createUserWithEmailAndPassword(ํšŒ์› ๊ฐ€์ž…) โœ๏ธ

code
  // ํšŒ์›๊ฐ€์ž…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜
  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");
  };
code
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(๋กœ๊ทธ์ธ) โœ๏ธ

code
 // ๋กœ๊ทธ์ธ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜
  const handleSignIn = async () => {
    try {
      await dispatch(
        signInUser({ email: signinInfo?.email, password: signinInfo?.password })
      ).unwrap();

      navigate("/home");
    } catch (error) {
      console.error(error);
    }
  };
code
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(ํšŒ์› ์œ ์ง€) โœ๏ธ

code
  useEffect(() => {
    dispatch(checkLoginUser(JSON.parse(localStorage.getItem("user"))));
  }, [dispatch]);
code
// ์œ ์ € ์ •๋ณด๋ฅผ ์„ค์ •ํ•˜๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜
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. ๋Œ€ํ‘œ ์ฝ”๋“œ โœ๏ธ

code
  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);
  };
code
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๋กœ ๋ถ„๋ฆฌ โœ๏ธ

code
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. ๋กœ๊ทธ์ธ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ๋ผ์šฐํŒ… ์˜ˆ์™ธ ์ฒ˜๋ฆฌ โœ๏ธ

code
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 ์ฒ˜๋ฆฌ โœ๏ธ

code
 <Button disabled={loading} onClick={handleSignIn}>
            Sign In
          </Button>
code
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๋ฅผ ํ†ตํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฒ˜๋ฆฌ โœ๏ธ

code
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

AI&ML ๊ธฐ์ดˆ

Reference: https&#x3A;//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์€ ๊ทธ