Roxylife
Roxy 빛나는 새벽
Roxylife
전체 방문자
오늘
어제
  • 분류 전체보기
    • 개발(회고록)현황일기
    • 개발CS지식
    • 알고리즘문제풀기
    • Git
    • 개발언어
      • HTML, CSS
      • JavaScript
    • 프론트엔드
      • React
    • Side Project
      • 혼자Project
      • 팀Project
    • 교육참여
      • [스파르타코딩클럽]
      • [저스트코드]

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 바닐라JS크롬앱
  • 드림코딩
  • git
  • JUSTCODE
  • 저스트코드6기
  • 코딩교육
  • 스파르타코딩클럽후기
  • 기업협업
  • 회고록
  • JUSTCODE6기
  • JavaScript
  • 개발
  • 신년운세코딩패키지
  • 노마드코더
  • 저스트코드
  • 스터디코드
  • 힙한취미코딩이벤트
  • 스파르타코딩클럽
  • 팀프로젝트
  • 코코아톡클론

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Roxylife

Roxy 빛나는 새벽

[회고록] 팀 프로젝트 1차_'미래식당' 을 참고하여 개발한 프로젝트   (220829 ~ 220908)
교육참여/[저스트코드]

[회고록] 팀 프로젝트 1차_'미래식당' 을 참고하여 개발한 프로젝트 (220829 ~ 220908)

2022. 9. 17. 01:54

1. 프로젝트 간략 소개

  • 참고 사이트 : 미래식당 https://meesig.com/
  • 사이트 소개 : 로컬 푸드 마켓 '미래식당' 사이트에 사용된 기능들을 참고하여 개발한 프로젝트
  • 선정 이유 : 웹서비스에서 보편적으로 사용되는 기능들을 이해하고 이를 React를 사용함으로써  웹페이지가 어떻게 구현이 될 수 있는지에 대한 궁금증을 풀어보며, React 숙련도 향상을 위해 구현하기 쉬운 E-commerce 사이트로 선정을 하였습니다.
  • 내가 참여한 코딩레스토랑팀 FE Github : https://github.com/Roxy100/justcode-6-1st-coding-restaurant-front
  • BE Github : https://github.com/wecode-bootcamp-korea/justcode-6-1st-coding-restaurant-back 
  • 개발 인원 : 프론트엔드 4명(박예선, 박찬영, 이은지, 임지영), 백엔드 2명(김민우, 이윤재)

 


 

2. 팀에서 내가 맡은 역할 및 해당 기능 설명

  • 시연 연상(ver. Full) : https://youtu.be/f1He2K5uwyI

전체 시연한 로그인, 회원가입 구현

 

 

주요한 해당 기능 설명 ( 이미지 첨부 및 코드 추가)

 

✅ 로그인, 회원가입, Footer

 

🔷 [공통] 소셜로그인 파트는 UI만 구현함. 아직 소셜계정을 통한 가입 및 로그인 기능은 구현은 제외.

🔷 [공통] Footer UI 구현. - 모든 페이지 footer 바탕색 흰색으로 통일하기로 수정.

첫 회원가입 페이지 UI와 Footer UI 구현
첫 로그인 페이지 UI 구현

 


🔷 이메일로 가입하기 버튼 눌렀을 때, form 입력창 펼치도록 구현

이메일로 가입하기 버튼 눌렀을 때, form 입력창 펼치면 나오는 UI구현

  • 해당 코드
         <div className={css['account-type-box']}>
            <a className={css.btn} onClick={emailSignUpClick}>
              이메일로 가입하기
            </a>
          </div>
          {emailSignUp && (
            <div className={css['member-by-email']}>
              <form
                className={css['account-form-body']}
                onSubmit={e => {
                  e.preventDefault();
                }}
              >
            ///....해당 form 입력창 부분들 생략
              </form>
            </div>
// 초기값으로 설정된 false값이 
const [emailSignUp, setEmailSignUp] = useState(false);

// 클릭했을 때 true로 바꿀 수 있게 해주는 함수
const emailSignUpClick = e => {
    setEmailSignUp(true);
};

 


🔷 회원가입 유효성 검사

  1. 이메일 - @포함, '.' 2개 이상 포함
  2. 비밀번호 - 10자리 이상
  3. 이름 - 한글 2~4자리
  4. 전화번호 - 010으로 시작, 10~11자리
  5. 생년월일 - 1900년대생, 올바른 월/일 입력 

🔷 위에 있는 유효성 검사에 맞지 않을 시, alert창으로 자세하게 안내말 적음.

🔷 유효성 검사가 다 맞을 시,

      ✔ 백엔드에서 요청한 메세지 'userCreated'가 맞을 시.

          alert창으로 '로그인 페이지로 이동합니다' 띄우고, Login페이지로 이동.

      ✔ 맞지 않을 시, alert창으로 '회원가입에 실패하였습니다' 띄움.

  // 각각 초기값으로 설정된 이메일,패스워드,재패스워드,이름,전화번호,생일,성별 state들
  const [isValid, setIsValid] = useState(false);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [rePassword, setRePassword] = useState('');
  const [name, setName] = useState('');
  const [phoneNumber, setPhoneNumber] = useState('');
  const [birth, setBirth] = useState('');
  const [gender, setGender] = useState('');
  const [femaleBorder, setFemaleBorder] = useState('#d3d3d3');
  const [maleBorder, setMaleBorder] = useState('#d3d3d3');
  
  // 이메일 input에 onChange함수
  const onEmailHandle = e => {
    setEmail(e.target.value);
  };
  
  // 패스워드 input에 onChange함수
  const onPasswordHandle = e => {
    setPassword(e.target.value);
  };

  // 재패스워드 input에 onChange함수
  const onRePasswordHandle = e => {
    setRePassword(e.target.value);
  };

  // 이름 input에 onChange함수
  const onNameHandle = e => {
    setName(e.target.value);
  };

  // 전화번호 input에 onChange함수
  const onPhoneNumberHandle = e => {
    setPhoneNumber(e.target.value);
  };

  // 생일 input에 onChange함수
  const onBirthHandle = e => {
    setBirth(e.target.value);
  };

  // 해당 성별을 클릭했을 때 나타나는 border색깔 
  const genderClick = e => {
    const genderValue = e.target.value;
    setGender(genderValue);
    if (genderValue == 'female') {
      setFemaleBorder('#bfaf96');
      setMaleBorder('#d3d3d3');
    } else if (genderValue == 'male') {
      setMaleBorder('#bfaf96');
      setFemaleBorder('#d3d3d3');
    }
  };
  
  // 이름, 전화번호, 생일은 정규식패턴으로 설정하여 보다 더 편하도록 변수로 설정.
  // 이름 - 한글 2~4자리
  const name_pattern = /^[가-힣]{2,4}$/; 
  // 전화번호 - 010으로 시작, 10~11자리
  const phone_pattern = /^010-?([0-9]{3,4})-?([0-9]{4})$/;
  // 생년월일 - 1900년대생, 올바른 월/일 입력 
  const birthday_pattern =
    /^(19[0-9][0-9]|20\d{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$/;

  // 이메일로 가입하기 클릭했을 때
  const userSignUp = () => {
    // 이메일 - @포함, '.' 2개 이상 포함
    email.includes('@') &&
    2 >= email.split('.').length - 1 >= 1 &&
    // 비밀번호 - 10자리 이상
    password.length >= 10
      ? setIsValid(true)
      : setIsValid(false);
    if (password !== rePassword) {
      alert('비밀번호를 확인하세요!');
    } else if (name_pattern.test(name) == false) {
      alert('이름은 2~4자리여야 합니다.');
    } else if (phone_pattern.test(phoneNumber) == false) {
      alert('전화번호는 010으로 시작하는 10~11자리여야 합니다.');
    } else if (birthday_pattern.test(birth) == false) {
      alert('생년월일 8자리를 확인해주세요.');
    } else if (!gender) {
      alert('성별을 확인해주세요.');
    } else {
      fetch('http://localhost:8000/signup', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email,
          password,
          name
          phoneNumber,
          birth,
          gender,
        }),
      })
        .then(response => response.json())
        .then(result => {
          if (result.message == 'userCreated') {
            alert(
              '미래식당의 회원이 되어주셔서 감사합니다:) \n로그인 페이지로 이동합니다.'
            );
            navigate('/login');
          } else {
            alert('회원가입에 실패하였습니다.');
          }
        });
    }
  };

 

 

🔷 로그인 유효성 검사

  1. 이메일 - @포함, '.'2개 이상
  2. 비밀번호 - 10자리 이상

🔷 위에 있는 유효성 검사에 맞지 않을 시, alert창으로 자세하게 안내말 적음.

🔷 유효성 검사가 다 맞을 시,

      ✔ email과 password를 localStorage에 보관하며,

          alert창으로 '로그인에 성공하였습니다' 띄우고, Main페이지로 이동.

      ✔ 로그인 실패시, alert창으로 '이메일과 비밀번호에 실패하였습니다.' 띄움.로그인, 회원가입 

 // 각각 초기값으로 설정된 이메일,패스워드 state들
  const [userEmail, setUserEmail] = useState('');
  const [userPassword, setUserPassword] = useState('');

  // 사용자이메일 input에 onChange함수
  const onUserEmailHandle = e => {
    setUserEmail(e.target.value);
  };

  // 사용자패스워드 input에 onChange함수
  const onUserPasswordHandle = e => {
    setUserPassword(e.target.value);
  };
  
  // 로그인하기 클릭했을 때 
  const userLogin = () => {
    if (
      // 이메일 - @포함, '.'2개 이상
      !userEmail.includes('@') ||
      userEmail.split('.').length - 1 < 1 ||
      userEmail.split('.').length - 1 > 2
    ) {
      alert('사용자 이메일이 맞지 않습니다.');
      // 비밀번호 - 10자리 이상
    } else if (userPassword.length < 10) {
      alert('비밀번호는 10자리이상이어야 합니다.');
    } else {
      fetch('http://localhost:8000/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email: userEmail,
          password: userPassword,
        }),
      })
        .then(response => response.json())
        .then(result => {
          if (result.token) {
            localStorage.setItem('token', result.token);
            alert('로그인에 성공하였습니다.');
            window.location.replace('/');
          } else {
            alert('이메일과 비밀번호를 찾을 수 없습니다.');
          }
        });
    }
  };

 


✅ 장바구니 기능

 

🔷 장바구니 기능 (조회, 삭제, 수정)

전체 시연한 장바구니 기능 (조회, 삭제, 수정)

 

주요한 해당 기능 설명 ( 코드 추가)

 

🔹 조회(GET) - 장바구니 페이지로 갔을 때, 장바구니에 담았던 데이터들을 보여주도록 구현.

 // fetch부분 구현 나에게 역할 추가
 useEffect(() => {
    fetch('http://localhost:8000/carts', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${localStorage.getItem('token')}`,
      },
    })
      .then(res => res.json())
      .then(req => {
        setUserName(req.data.name);
        setCartList(req.data.cartList);
        setItemTotal(req.data.totalPrice);
        setTotalDelivery(req.data.deliveryFee);
        setTotalPrice(req.data.orderPrice);
      });
  }, [itemState]);

 

🔹 삭제(DELETE) - 삭제 버튼 눌렀을 때, 상품 목록이 사라지는 구현.

  const deleteClick = () => {
    // 여기부터 fetch부분 구현 나에게 역할 추가 
    fetch('http://localhost:8000/carts', {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${localStorage.getItem('token')}`,
      },
      body: JSON.stringify({
        cartsId: item.id,
      }),
    }).then(res => res.json());
    // 여기까지...
    itemState === true ? setItemState(false) : setItemState(true);
    setCartCount(cartCount - 1);
  };

 

🔹 수정(PATCH) - 수량을 + or - 할 때, 상품의 수량이 변화되는 구현.

  const countPlus = () => {
    let newPrice = Number(`${(count + 1) * item.price}`);
    setCount(count + 1);
    setItemTotalPrice(newPrice); 
    // 여기부터 fetch부분 구현 나에게 역할 추가
    fetch('http://localhost:8000/carts', {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${localStorage.getItem('token')}`,
      },
      body: JSON.stringify({
        cartsId: item.id,
        quantity: 1,
      }),
    }).then(res => res.json());
    // 여기까지...
    itemState === true ? setItemState(false) : setItemState(true);
  };
  
  const countMinus = () => {
    count >= 2 && setCount(count - 1);
    count >= 2 && setItemTotalPrice(Number(`${(count - 1) * item.price}`));
    count >= 2 &&
      // 여기부터 fetch부분 구현 나에게 역할 추가
      fetch('http://localhost:8000/carts', {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${localStorage.getItem('token')}`,
        },
        body: JSON.stringify({
          cartsId: item.id,
          quantity: -1,
        }),
      }).then(res => res.json());
      // 여기까지...
    itemState === true ? setItemState(false) : setItemState(true);
  };

 

나는 '미래식당' 사이트를 봤을 때, 내가 자신있게 구현할 수 있는 건, 로그인, 회원가입 페이지들이었다.

Justgram할 때 배웠던 거 복습하고자 아직 함수 메소드 map등 쓰는 게 자신이 없기 때문에 그 페이지들을 먼저 선택했다. 팀원분들과 상의하면서 디테일한 UI는 제외하고, 큰 틀 레이아웃 중심으로 짜보면서, 모르는 것은 팀원분들께 물어보기도 하고, 소셜로그인 파트는 제대로 된 기능은 하지 않지만, UI만 만들어 놓는 걸 중심으로 했다. 나중에, 소셜계정을 통한 가입 및 로그인 기능은 더 공부해서 구현하도록 해야겠다. 

그리고, 거의 완성되어갈 쯤, Footer UI 구현도 말끔히 2~3시간 만에 구현하고, 다른 분이 맡으신 장바구니페이지(UI만 작업완료된 상태)에서 장바구니 기능으로 조회, 삭제, 수정을 하는 작업이 나에게 추가되어, fetch를 이용한 코드 공부(구글링)도 하면서 작은 보탬이 되어 보다 더 빠른 상황으로 나갈 수 있었다. 그리고 계속적으로 팀원분들과도 소통하면서, 버그 픽스 및 리팩토링도 진행하였다.

 


3. 기억하고 싶은 코드

(1) 가장 어려웠던 코드 및 아쉬운 코드

  const name_pattern = /^[가-힣]{2,4}$/;
  const phone_pattern = /^010-?([0-9]{3,4})-?([0-9]{4})$/;
  const birthday_pattern =
    /^(19[0-9][0-9]|20\d{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$/;

  const userSignUp = () => {
    email.includes('@') &&
    2 >= email.split('.').length - 1 >= 1 &&
    password.length >= 10
      ? setIsValid(true)
      : setIsValid(false);
    if (password !== rePassword) {
      alert('비밀번호를 확인하세요!');
    } else if (name_pattern.test(name) == false) {
      alert('이름은 2~4자리여야 합니다.');
    } else if (phone_pattern.test(phoneNumber) == false) {
      alert('전화번호는 010으로 시작하는 10~11자리여야 합니다.');
    } else if (birthday_pattern.test(birth) == false) {
      alert('생년월일 8자리를 확인해주세요.');
    } else if (!gender) {
      alert('성별을 확인해주세요.');
    }
  • 익숙하지 않은 정규식 코드로 유효성 검사하는 것 자체가 어려웠다. 그래서 구글링을 통해, 회원가입 유효성 검사 구현 중, 이름, 전화번호, 생년월일은 반드시 정규식 코드로 정해야 더 편했고, 이것 때문에 정규식 문법을 살짝 맛보면서 표시방법들도 알게 되었다. 그 외, 다른 부분은 때에 따라 유효성 검사가 틀릴 시, alert창으로 자세하게 안내창을 구현하면서, 팀원들이 봤을 때, 효율적으로 알게 한 점이 뿌듯했다.
  • 현재, if, else if 경우의 수마다 alert창으로 코드를 짰는데... 이보다 더 효율적인 코딩을 짜 볼 아이디어가 있었으면 좋겠다.

 

(2) 깨닫고 싶은 코드

         <div className={css['account-type-box']}>
            <a className={css.btn} onClick={emailSignUpClick}>
              이메일로 가입하기
            </a>
          </div>
          {emailSignUp && (
            <div className={css['member-by-email']}>
              <form
                className={css['account-form-body']}
                onSubmit={e => {
                  e.preventDefault();
                }}
              >
                <div className={css['user-email']}>
                  <label htmlFor="userId" className={css.string}>
                    아이디(이메일)*
                  </label>
                  <input
                    id="userId"
                    type="email"
                    name="userId"
                    value={email}
                    placeholder="이메일을 입력해주세요."
                    required={true}
                    className={css['input-text']}
                    onChange={onEmailHandle}
                  />
                </div>
  • 이 기능은 팀원분께 도움을 요청하여 코드를 짰다. && 연산자를 통해 emailSignup 함수가 실행(true)시. 다음 코드들이 보여지게 되는(입력창들이 펼치게 되는) 기능이 된다는 게 신기하고 보다 더 && 연산자 (조건부 렌더링)를 이해하게 되었다.
  • 👌 label input 태그를 사용해서 그런지, javascript에서는 label for 사용하지만, JSX문법에서는 label htmlFor 을 사용해야 되는 것도 알게 되었다.

 


4. 프로젝트를 마치고 느낀 점

협업

  • 1차 프로젝트 때 느낀 건, Git, Github사용할 때 많이 느꼈다. 팀원분들과 상의하여, branch(기능별)와 Commit 컨벤션에 맞춰 하다보니 적응해가면서 보다 더 효율적인 작업(pr이나 원격 main에 커밋 시)이 이루어졌다. 그러나, 멋모르고 터미널에 적었던 명령어가 멘토님 세션 이후 흐름이 확 이해가 갔다. 프로젝트 초반에는 Conflict가 일어나면서, 대체 어떡하지?? 하고서 멘토님 말씀에 따라 그냥 실행한 반면, Git 명령어 흐름에 대한 이해 후, 어떤 터미널 메세지가 와도 당황하지 않고, 차근차근 해결해 나갈 수 있는 마음가짐과 자세를 가지게 되었다.
  • [공통] 우리 팀은 작업 진행상황 관리를 Trello보드로 이용하다보니, 보다 더 확연한 진행상황을 볼 수 있었고, 팀원분들의 상황을 자주 공유하면서 (즉, 미니미팅 제도를 도입하여) 서로 도와가면서 프로젝트를 진행할 수 있었다. 그 이유는 팀프로젝트가 다들 처음이다 보니, 첫 플래닝 미팅 때, 전반적인 프로젝트의 흐름을 어떻게 계획해야 하는지에 대한 어려움이 있었다. 나름대로 일정을 계획하였으나, 실제로 진행하다보니 변동사항이 많았기에 잦은 소통이 필요했기 때문이다.
  • API 작업시, 백엔드분들과 통신을 하면서, 잦은 에러도 잡고, fetch API구조에 대해서 공부를 많이 하게 된 시간이었다. 단순히 통신만 하는 게 아니라 서로 어떤 방식으로 동작하는지 이해하는 과정이 좋았다.
  • 다음 2차 프로젝트 때는 백엔드분들과도 프로젝트 초기에 db의 데이터들을 잘 정해놓고, 서로 어떤 것들을 미리 알려줘야 나중에 고칠 일 없도록 해야 좋은 진행으로 나갈 수 있을 것 같다.

 

개인

  • 언젠가 중요한 로그인, 회원가입 기능을 맡아야 다음 프로젝트 때, 그 외 다른 부분들을 맡았을 때 페이지에 대한 이해가 가기 위해서 이 부분들을 선택했었다. 막상 이 부분들을 맡다보니, 하나하나 생각하면서 코딩짜는 게 그리 쉽지 않다는 걸 느꼈다. 팀원분의 조언을 참고해서 하나씩하나씩 그려나가는 연습 및 해야 할 순서정리들을 배워본 것 같다.
  • 👌 그리고 시간의 여유가 생기더라도, 내가 맡은 부분이 아닐지라도, 다른 사람이 짜놓은 코드를 보고 읽으며 피드백을 준다거나 다른 사람을 도와주면서, 담당자의 설명을 듣고 이해하는 능력이 필요하다는 것을 알 수 있었다.
  • 다음 2차 프로젝트 때는 보다 더 도전적인 작업을 할 수 있도록 어떤 것이 오든 부딪혀서 '나는 구현할 수 있다!'라는 마음을 가지고 해야 되겠다.

 

기술스택을 사용하면서...

이 프로젝트에서 백엔드와 통신하기 위한 준비로 Fetch를 사용하며 내 기억속에 많이 남아 경험을 들려주고자 한다.
  • API문서에 따르면, 내가 코드에 적용할 메서드가 GET, POST, DELETE, PATCH이기 때문에 headers와 body값에 어떤 게 들어가있는지 공부하며 코드를 짜기 위해, 'fetch {해당 메서드}'이라고 구글링해서 여러 블로그의 예시들을 보면서 공부했었다. 
공부한 바로는,
  • 회원가입, 로그인 할 때는 POST, GET 이기 때문에 headers에 request에 보내는 데이터(body)의 타입정보를 표현하기 위해
 headers: {
        'Content-Type': 'application/json',
      },

를 하는 것이었고,

  • 장바구니 조회, 삭제, 수정할 때는 이미 로그인한 상태 즉, token값 정보도 포함이 된 데이터를 보내기 위해, Authorizaiton(인증) 정보도 필요하다는 것을 알 수 있었다.
 headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${localStorage.getItem('token')}`,
      },

 

💦 표현방법에 있어서...

처음에는 다른사람이 짜놓은 코드에서 통신테스트를 하면서 에러상황을 맞닺뜨렸는데.. 원인은 스펠링이 틀려서 나타난 문제이기도 했었다. (Authorization 스펠링)

Authorization: `Bearer ${localStorage.getItem('token')}`

 

👌 아하! 모먼트

처음 Foundation기간에 fetch를 사용할 때, headers에 왜 저렇게 적는지 생각을 안하고 코드복붙하면서 짰다가,

이번 프로젝트 때 문서도 찾아보며 깊이 fetch구조에 대해 탐구해보면서 앞으로 코드를 짤 때는 기본 구조에 대해 탐구해보고 프로그래밍적 사고를 생각하면서 짜야한다는 걸 느꼈다. 그리고 테스트하기 전에 꼭 스펠링 문제가 없는지 확인한 뒤 해봐야 그 다음 과정에 무리가 없을 것 같다.

 

✨ fetch 구글링을 하게 되면...

같이 나오는 키워드가 axios 인데, 현재는 fetch사용하는 것에 익숙해질 정도로 공부했다면, 다른 프로젝트에서는 axios를 써보는 것도 좋을 것 같다. 왜냐하면 2개를 사용하면 장단점이 명확해지기 때문에 보다 더 앞으로 펼쳐질 프로젝트에서 사용할 스택을 정할 때 도움이 될 것 같다는 생각이 든다. 

'교육참여 > [저스트코드]' 카테고리의 다른 글

[회고록] 기업협업 프로젝트 3rd 개인과제 (221011 ~ 221019)  (0) 2022.10.23
[회고록] 기업협업 프로젝트 2nd 과제 (221007 ~221009)  (0) 2022.10.23
[회고록] 기업협업 팀프로젝트 1st 과제 (221004 ~ 221006)  (0) 2022.10.10
[회고록] 팀 포르젝트 2차_'FLO'을 참고하여 개발한 프로젝트 (220919 ~ 220930)  (0) 2022.10.10
[저스트코드 회고록] 팀 프로젝트 들어가기 전 (220707 ~ 220826)  (1) 2022.09.13
    '교육참여/[저스트코드]' 카테고리의 다른 글
    • [회고록] 기업협업 프로젝트 2nd 과제 (221007 ~221009)
    • [회고록] 기업협업 팀프로젝트 1st 과제 (221004 ~ 221006)
    • [회고록] 팀 포르젝트 2차_'FLO'을 참고하여 개발한 프로젝트 (220919 ~ 220930)
    • [저스트코드 회고록] 팀 프로젝트 들어가기 전 (220707 ~ 220826)
    Roxylife
    Roxylife
    꿈나무 FE개발자

    티스토리툴바