FrontEnd Develop/Project : Wallet Guardians

๋ณธ๊ฒฉ ์—ฐ๋™ #4 ๋กœ๊ทธ์ธ ํ›„ ๊ธฐ์กด ์ปจํ…์ŠคํŠธ(๋ฐ์ดํ„ฐ)๊ฐ€ ์žˆ์œผ๋ฉด ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋ฐฉ๋ฒ• (React + Context API + Axios)

Frisbeen 2025. 2. 10. 02:28

๐Ÿš€ ๊ฐœ์š” ๋ฐ ๋ฌธ์ œ์ƒํ™ฉ

์‚ฌ์šฉ์ž๊ฐ€ ์ฒ˜์Œ ํšŒ์›๊ฐ€์ž… ํ›„ ๋กœ๊ทธ์ธ์„ ํ•˜๋ฉด, ๋งค๋‹ฌ ์ž์‹ ์˜ ์ฒซ ๊ฐ€๊ณ„๋ถ€์— ํ•„์š”ํ•œ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. 

์ €๋Š” ์ด ์˜ˆ์‚ฐ์„ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๋กœ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. -> goalContext.jsx

 

๊ทธ๋Ÿฌ๋‚˜, ๋ถ„๋ช… ๋กœ๊ทธ์ธ ๋กœ์ง์—๋Š” goalAmount๊ฐ€ ์žˆ์œผ๋ฉด -> mainPage.jsx, ์—†์œผ๋ฉด -> goal-setting.jsx๋กœ ๊ฐ€๊ฒŒ๋” ์„ค๊ณ„๋ฅผ ํ–ˆ๋Š”๋ฐ

์ œ ์ฝ”๋“œ์˜ ์˜๋„์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ๊ณ„์† ์˜ค๋ฅ˜๊ฐ€ ๋‚ฌ์Šต๋‹ˆ๋‹ค.

 

์‹ฌ์ง€์–ด, ์ด๋ฏธ ๋กœ๊ทธ์ธ์„ ํ•˜์—ฌ ์˜ˆ์‚ฐ ์„ค์ •์„ ํ•œ ์‚ฌ์šฉ์ž๋„ ๋‹ค์‹œ goal-setting์œผ๋กœ ๊ฐ€๋Š” ๋ฌธ์ œ์ƒํ™ฉ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

 

-> ์„œ๋ฒ„(๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž)๋Š” ํ•œ ๋‹ฌ์— ๋”ฑ ํ•œ๋ฒˆ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ• ์ˆ˜ ์žˆ๊ฒŒ ์„ค๊ณ„๋ฅผ ํ–ˆ๋Š”๋ฐ ์ด ๋•œ์— ๋‘ ๋ฒˆ ์˜ˆ์‚ฐ ์„ค์ •์„ ํ•˜๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋Š” ์ตœ์•…์˜ ์ƒํ™ฉ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ •๋ฆฌํ•˜๋ฉด

1. ์˜๋„์น˜์•Š์€ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ๋ถˆ๊ฐ€

2. 1๋ฒˆ์œผ๋กœ ์ธํ•ด ์„œ๋ฒ„์™€์˜ ์•ฝ์†์„ ์ง€ํ‚ค์ง€ ๋ชปํ•˜๊ณ  ์˜๋„์™€ ๋‹ค๋ฅธ ์˜ค๋ฅ˜๋ฐœ์ƒ!

 

๐Ÿ›  ํ•ด๊ฒฐํ•ด์•ผ ํ•  ๊ธฐ๋Šฅ

 

โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„, ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ์กด์— ์„ค์ •ํ•œ ์˜ˆ์‚ฐ์ด ์žˆ๋‹ค๋ฉด ๋ฐ”๋กœ ๋ฉ”์ธ ํŽ˜์ด์ง€(/main)๋กœ ์ด๋™

โœ… ์‚ฌ์šฉ์ž๊ฐ€ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด /goal-setting ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜์—ฌ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜๋„๋ก ์œ ๋„

โœ… ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ(goalAmount)๊ฐ€ ์œ ์ง€๋˜๋„๋ก localStorage๋ฅผ ํ™œ์šฉ

โœ… ๋น„๋™๊ธฐ ์š”์ฒญ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „์— ํŽ˜์ด์ง€๊ฐ€ ์ด๋™ํ•˜๋Š” ๋ฌธ์ œ ๋ฐฉ์ง€

โœ… ๋ถˆํ•„์š”ํ•œ API ์š”์ฒญ์„ ์ตœ์†Œํ™”ํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”

 

 

๐Ÿ”น 1. ํ•„์š”ํ•œ ๊ฐœ๋… ์ •๋ฆฌ

 

๐Ÿ“Œ 1๏ธโƒฃ ๋กœ๊ทธ์ธ ํ›„ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•ด์•ผ ํ•˜๋Š” ์ด์œ 

 

์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๋ฉด,

1. ๊ธฐ์กด ์˜ˆ์‚ฐ์ด ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ๋ฉ”์ธ ํŽ˜์ด์ง€(/main) ๋กœ ์ด๋™

2. ๊ธฐ์กด ์˜ˆ์‚ฐ์ด ์—†์œผ๋ฉด ์˜ˆ์‚ฐ ์„ค์ • ํŽ˜์ด์ง€(/goal-setting) ๋กœ ์ด๋™ํ•˜์—ฌ ์ง์ ‘ ์„ค์ •ํ•˜๋„๋ก ์œ ๋„

 

์ด๋ฅผ ์œ„ํ•ด ๋กœ๊ทธ์ธ → ์˜ˆ์‚ฐ ์กฐํšŒ → ํŽ˜์ด์ง€ ์ด๋™ ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰ํ•ด์•ผ ํ•จ.

 

 

๐Ÿ“Œ 2๏ธโƒฃ ์šฐ๋ฆฌ๊ฐ€ ๊ฒช์—ˆ๋˜ ๋ฌธ์ œ (์‚ฝ์งˆํ–ˆ๋˜ ๋ถ€๋ถ„ ๐Ÿ˜‚)

 

โŒ ๋กœ๊ทธ์ธ ํ›„ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ(goalAmount)๋ฅผ ํ™•์ธํ•˜๊ธฐ ์ „์— ํŽ˜์ด์ง€๊ฐ€ ๋จผ์ € ์ด๋™goalAmount === null๋กœ ์ธ์‹๋˜์–ด /goal-setting์œผ๋กœ ์ด๋™

โŒ ๋น„๋™๊ธฐ API ์š”์ฒญ(fetchBudget())์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „์— goalAmount๋ฅผ ๊ฒ€์‚ฌํ•˜๋ฉด ์ž˜๋ชป๋œ ๊ฐ’์ด ์‚ฌ์šฉ๋  ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ

โŒ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด goalAmount๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์–ด /goal-setting์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋ฌธ์ œ ๋ฐœ์ƒ

 

์ฒ˜์Œ goalAmount ์ „์—ญ ์ƒํƒœ์˜ ์ดˆ๊ธฐ๊ฐ’์„ null๋กœ ์ •์˜ํ–ˆ๊ธฐ์— ๋ฐœ์ƒํ•œ ๋ฌธ์ œ. ( ๊ทธ๋Ÿฌ๋‚˜ null๋กœ ์œ ์ง€๋Š” ํ•ด์•ผํ•จ)

import { createContext, useState } from 'react';
import { getBudget } from '../api/budgetApi';

export const GoalContext = createContext();

export const GoalProvider = ({ children }) => {
  const [goalAmount, setGoalAmount] = useState(null);
  const [error, setError] = useState(null);

  // โœ… ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
  const fetchBudget = async () => {
    try {
      const data = await getBudget();
      console.log('โœ… ์œ ์ € ์™ธ๊ณฝ์˜ค์นด๋„ค ์ •๋ณด:', data);
      console.log('โœ… ์œ ์ € ์˜ค์นด๋„ค ์ •๋ณด:', data.amount);
      setGoalAmount(data.amount);
      return data.amount;
      //`${userBudget.toLocaleString()} ์›` -> ์ฝค๋งˆ ์ฐํžˆ๋Š”๊ฑฐ์ž„
    } catch (error) {
      console.error('๐Ÿšจ ์˜ˆ์‚ฐ ์กฐํšŒ ์‹คํŒจ:', error);
      setError(error);
    }
  };

  return (
    <GoalContext.Provider
      value={{ goalAmount, setGoalAmount, fetchBudget, error }}
    >
      {children}
    </GoalContext.Provider>
  );
};

export default GoalProvider;

 

๐Ÿ”น 2. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• (์ „์ฒด์ ์ธ ํ๋ฆ„)

 

โœ… 1๏ธโƒฃ ๋กœ๊ทธ์ธ ํ›„ goalAmount ๊ฐ’์„ ๋น„๋™๊ธฐ ์š”์ฒญ์œผ๋กœ ๊ฐ€์ ธ์˜ด 

๋กœ๊ทธ์ธ ํ›„ fetchBudget()์„ ์‹คํ–‰ํ•˜์—ฌ ์˜ˆ์‚ฐ์„ ๊ฐ€์ ธ์˜ด.

 

โœ… 2๏ธโƒฃ goalAmount๋ฅผ localStorage์— ์ €์žฅํ•˜์—ฌ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ๊ฐ’ ์œ ์ง€ -> ํ•ต์‹ฌ ์•„์ด๋””์–ด!! 

goalAmount ๊ฐ’์„ localStorage์— ์ €์žฅํ•˜๊ณ , useState์˜ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ํ™œ์šฉ.

๋˜ํ•œ ์ถ”๊ฐ€์ ์œผ๋กœ, ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ† ํฐ๋งŒ ์ง€์› ๋Š”๋ฐ ์ด์ œ goalAmount๊ฐ’๋„ ์ง€์›Œ์•ผํ•จ

 

โœ… 3๏ธโƒฃ goalAmount ๊ฐ’์ด null์ธ์ง€ ์•„๋‹Œ์ง€ ํ™•์ธํ•œ ํ›„ ์ ์ ˆํ•œ ํŽ˜์ด์ง€๋กœ ์ด๋™

๊ฐ’์ด ์žˆ์œผ๋ฉด /main์œผ๋กœ ์ด๋™, ์—†์œผ๋ฉด /goal-setting์œผ๋กœ ์ด๋™.

 

๐Ÿ“Œ 3. ์‹ค์ œ ์ฝ”๋“œ ๊ตฌํ˜„

 

๐Ÿ”น Step 1๏ธโƒฃ . GoalContext.jsx (์˜ˆ์‚ฐ ์ƒํƒœ ๊ด€๋ฆฌ)

goalAmount ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , API์—์„œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ• 

useEffect()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๊ทธ์ธ ํ›„ fetchBudget()์„ ์‹คํ–‰

localStorage๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ๋ฐ์ดํ„ฐ ์œ ์ง€

import { createContext, useState, useEffect } from 'react';
import { getBudget } from '../api/budgetApi';

export const GoalContext = createContext();

export const GoalProvider = ({ children }) => {
  const [goalAmount, setGoalAmount] = useState(() => {
    // โœ… localStorage์—์„œ ๊ธฐ์กด ์˜ˆ์‚ฐ ๊ฐ’์„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ (์ƒˆ๋กœ๊ณ ์นจ ์‹œ ๊ฐ’ ์œ ์ง€)
    const savedGoal = localStorage.getItem('goalAmount');
    return savedGoal ? JSON.parse(savedGoal) : null;
  });

  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // โœ… ๋ชฉํ‘œ ๊ธˆ์•ก์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
  const fetchBudget = async () => {
    setLoading(true);
    try {
      const data = await getBudget(); // API ํ˜ธ์ถœ
      setGoalAmount(data.amount);
      localStorage.setItem('goalAmount', JSON.stringify(data.amount)); // โœ… localStorage ์ €์žฅ
    } catch (error) {
      console.error('๐Ÿšจ ์˜ˆ์‚ฐ ์กฐํšŒ ์‹คํŒจ:', error.response?.data || error.message);
      setError(error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    const accessToken = localStorage.getItem('accessToken');
    if (accessToken) {
      fetchBudget();
    }
  }, []);

  return (
    <GoalContext.Provider value={{ goalAmount, setGoalAmount, fetchBudget, loading, error }}>
      {children}
    </GoalContext.Provider>
  );
};

export default GoalProvider;

 

 

๐Ÿ”น Step 2๏ธโƒฃ LoginPage.js (๋กœ๊ทธ์ธ ํ›„ ์˜ˆ์‚ฐ ์ฒดํฌ & ํŽ˜์ด์ง€ ์ด๋™)

๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„, fetchBudget()์„ ํ˜ธ์ถœํ•˜์—ฌ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ํ™•์ธ (์—ฌ๊ธด ์ž๋ช…ํ•จ)

 

๋น„๋™๊ธฐ ์š”์ฒญ์ด ๋๋‚  ๋•Œ๊นŒ์ง€ goalAmount ๊ฐ’์„ ๊ธฐ๋‹ค๋ฆฐ ํ›„ ํŽ˜์ด์ง€ ์ด๋™ -> const budgetAmount = await fetchBudget()

-> ์—ฌ๊ธฐ์—๋„ ์•„์ฃผ ์ข‹์€ ๋‚ด์šฉ๋“ค์ด ๋งŽ์Œ. ( ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ ์ด๋ผ๋˜์ง€ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์˜ ์ดํ•ด)

 

goalAmount๊ฐ€ ์žˆ์œผ๋ฉด /main์œผ๋กœ, ์—†์œผ๋ฉด /goal-setting์œผ๋กœ ์ด๋™

 

import { useContext, useState, useEffect } from 'react';
import '../style/LoginPage.scss';
import SignupPage from './SignupPage';
import { useNavigate } from 'react-router-dom';
import { GoalContext } from '../context/GoalContext';
import { login } from '../api/authApi.jsx';
import { css, keyframes } from '@emotion/react';

// ๋กœ๋”ฉ ์‹œ ํ•„์š”ํ•œ
const spin = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`;

const spinnerStyle = css`
  display: inline-block;
  width: 24px;
  height: 24px;
  border: 3px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  border-radius: 50%;
  animation: ${spin} 1s linear infinite;
  margin-left: 10px;
`;

const LoginPage = () => {
  const [isSignupOpen, setIsSignupOpen] = useState(false); // ํšŒ์›๊ฐ€์ž… ๋ชจ๋‹ฌ ์ƒํƒœ
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false); // ๋กœ๋”ฉ ์ƒํƒœ ์ถ”๊ฐ€
  const [modalMessage, setModalMessage] = useState({ type: '', message: '' });
  const [fetchingBudget, setFetchingBudget] = useState(false); //์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ณ ์žˆ๋Š”์ง€?
  const navigate = useNavigate();
  const { goalAmount, fetchBudget } = useContext(GoalContext); //getํ•˜๋Š” ํ•จ์ˆ˜๋„ ๊ฐ€์ ธ์˜ค์ž.

  // ํšŒ์›๊ฐ€์ž… ๋ชจ๋‹ฌ ์—ฌ๋‹ซ๊ธฐ
  const openSignupModal = () => setIsSignupOpen(true);
  const closeSignupModal = () => setIsSignupOpen(false);

  // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
  const handleLogin = async (e) => {
    e.preventDefault();
    setLoading(true); // ๋กœ๊ทธ์ธ ์‹œ์ž‘ ์‹œ ๋กœ๋”ฉ ์ƒํƒœ ํ™œ์„ฑํ™”

    try {
      const data = await login(email, password);
      // const { token, refreshToken } = data; (ํ† ํฐ key๋ช… ๋ฐ”๊ฟˆ)
      const { accessToken, refreshToken } = data.data;
      console.log('๐Ÿ”น ๋กœ๊ทธ์ธ ์‘๋‹ต ๋ฐ์ดํ„ฐ:', data); // ์‘๋‹ต ๊ตฌ์กฐ ํ™•์ธ ๋””๋ฒ„๊น…์šฉ์ž„
      console.log('๐Ÿ”‘ ์ €์žฅํ•  accessToken:', accessToken);
      console.log('๐Ÿ”„ ์ €์žฅํ•  refreshToken:', refreshToken);

      //๋กœ๊ทธ์ธ ์„ฑ๊ณต์‹œ ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์—์„œ ๋ฐ›์•„์˜จ ํ† ํฐ๋“ค ์ €์žฅ.
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);

      setModalMessage({
        type: 'success',
        message: '๋กœ๊ทธ์ธ ์„ฑ๊ณต! ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค!',
      });

      setFetchingBudget(true);
      const budgetAmount = await fetchBudget(); //๋กœ๊ทธ์ธ ์„ฑ๊ณตํ•˜๋ฉด ๊ฐ–๊ณ  ์žˆ๋Š” ์˜ˆ์‚ฐ ์žˆ๋Š”์ง€ ํ™•์ธ
      setFetchingBudget(false);

      console.log('๐Ÿฆ ๋กœ๊ทธ์ธ ํ›„ ๋ฐ›์€ ์˜ˆ์‚ฐ ๊ธˆ์•ก:', budgetAmount); // ๋””๋ฒ„๊น…์šฉ ์ฝ”๋“œ์ž„

      //  goalAmount๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๊ธฐ ์ „์— budgetAmount๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŽ˜์ด์ง€ ์ด๋™ (์„ฑ๋Šฅ ๊ฐœ์„  ๊ฐ€๋Šฅ์„ฑ? )
      setTimeout(() => {
        if (budgetAmount !== null && budgetAmount > 0) {
          navigate('/main');
        } else {
          navigate('/initial');
        }
      }, 500);
    } catch (error) {
      console.error('๋กœ๊ทธ์ธ ์‹คํŒจ:', error);
      setModalMessage({
        type: 'error',
        message: '๋กœ๊ทธ์ธ ์‹คํŒจ! ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
      });
    } finally {
      setLoading(false); // ์‘๋‹ต ์™„๋ฃŒ ํ›„ ๋กœ๋”ฉ ํ•ด์ œ
    }
  };

  return (
    <div className="login-page-container">
      <h1>Login Page</h1>
      <p>๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ํ•ด์ฃผ์„ธ์š”.</p>

      <form className="form" onSubmit={handleLogin}>
        <input
          type="email"
          placeholder="์ด๋ฉ”์ผ"
          className="input-field"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          disabled={loading}
        />
        <input
          type="password"
          placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ"
          className="input-field"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          disabled={loading}
        />
        <button
          type="submit"
          className="login-button"
          disabled={loading || fetchingBudget}
        >
          {loading ? '๋กœ๋”ฉ ์ค‘...' : '๋กœ๊ทธ์ธ'}
          {loading && <span css={spinnerStyle} />}
        </button>
        {fetchingBudget && (
          <div className="loading-container">
            <p>์˜ˆ์‚ฐ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...</p>
            <span css={spinnerStyle} />
          </div>
        )}
        {modalMessage.message && (
          <div className={`modal-message ${modalMessage.type}`}>
            {modalMessage.message}
          </div>
        )}
      </form>

      <p className="sign-up-prompt">
        ๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”?{' '}
        <span className="sign-up-link" onClick={() => setIsSignupOpen(true)}>
          ํšŒ์›๊ฐ€์ž…
        </span>
      </p>

      {isSignupOpen && (
        <SignupPage closeSignupModal={() => setIsSignupOpen(false)} />
      )}
    </div>
  );
};

export default LoginPage;

 

๐Ÿ”น Step 3๏ธโƒฃ MainPage.js (๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™)

 

๐Ÿš€ ์ตœ์ข… ๋™์ž‘ ํ™•์ธ

 

โœ”๏ธ ๋กœ๊ทธ์•„์›ƒํ•˜๋ฉด accessToken, refreshToken, goalAmount ๋ชจ๋‘ ์‚ญ์ œ๋จ

โœ”๏ธ ๋กœ๊ทธ์•„์›ƒ ํ›„ /login ํŽ˜์ด์ง€๋กœ ์ •์ƒ ์ด๋™

โœ”๏ธ ์ƒˆ๋กœ์šด ๋กœ๊ทธ์ธ ์‹œ ๊ธฐ์กด goalAmount ๊ฐ’์ด ๋‚จ์•„์žˆ์ง€ ์•Š์•„ ์˜ฌ๋ฐ”๋ฅธ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ๋จ