2nd Team Project Retrospective - Dobby(DIY : Do it Yourself)

Seong Ho Kim·2023년 11월 4일
0
post-thumbnail

💻 2차 프로젝트 소개

Dobby (Do it Yourself)

  • Dobby 프로젝트는 "DIY를 취미로 즐겨라" 라는 뜻을 가지고 다양한 취미생활의 새로운 경험을 제공하기 위한 구독형 서비스 팀 프로젝트 입니다.

🖥️ 프로젝트 시연영상

Dobby Project 시연 영상

🖥️ Project Github 저장소

Front-End Github - Dobby Project

🛜 팀 프로젝트 기간 & 참여인원

  • 날짜 : 2023년 10월 23일 ~ 2023년 11월 3일 (총 2주)
  • 참여 인원 : 8명 (프론트엔드 4명, 백엔드 4명)
  • Team call sign : B팀
  • 팀원 소개

    Product Manager : 홍지영(Front-End)
    Project Manager : 유진서(Back-End)
    Template Designer : 김성호(Front-End)
    Front-End Teammate : 김성호, 박주희, 최민지
    Back-End Teammate : 조민수, 김영범, 최현수

🛠️ 기술 Stack & Tool

  • Front-End Stack
    React, Javascript, HTML, SCSS

  • Back-End Tool
    Javascript, Node.js, Express, MySQL

  • Communication Tool
    Git & Github, Slack, Notion, Trello

  • Code Program
    Terminal, Visual Studio Code

🧐 PET(Product, End-User, Tech) 분석

제품 벤치마킹 사이트

1) 로그인/회원가입

우리 서비스 회원으로의 유입과 전환을 책임져 줄 웹서비스 기술을 구현.

  • 약관 필수, 선택사항 추가
  • 간편 로그인 시스템에는 어떤것이 있을까?
  • 고객의 회원 정보는 어디까지 있어야 할까?
    (이름, 생년월일, 이메일, 주소, 연락처, 닉네임)
  • 로그인, 로그아웃을 원활히 하기 위해 어떻게 웹 페이지를 구성해야할까?

2) 제품

우리 서비스가 판매할 가상의 제품을 분석하고 이를 판매할 기술을 구현.

  • 우리의 웹 서비스를 경유하여 판매하는 제품은 어떻게 정의할 수 있을까?
  • 우리의 웹 서비스 매출은 자사 제품 판매를 통해 이루어지는가?
  • 기획자는 어떠한 의도로 해당 웹 페이지를 주어진 화면처럼 구성한 것일까?
    (수익성, 광고성 등)
  • 그리고 기획자의 의도를 개발자가 기술적으로 구현하기 위해 분석해야할 요소는 무엇 무엇이 있는가?
    (Filtering, Pagination, Sorting, 모달 창, 팝업 창 구현 등)
  • 전체 제품을 노출하는 리스트 페이지와 상세 페이지가 담는 제품 데이터의 수와 그 양은 어떤 차이가 있는가?

3) 결제

고객의 실제 매출로 이루어질 수 있는 결제 절차를 기술적으로 구현.

  • 결제 프로세스에서 고객을 위해 꼭 노출 시켜야할 정보는 무엇무엇이 있는가?
  • 결제 시 배송지를 적는란이 있는가? 결제 프로세스에서 수집해야하는 정보가 너무 많을 경우, 로그인/회원가입 - 제품 - 주문의 절차 중 해당 내용을 미리 받아내어 결제 허들을 낮추는 방법이 있지 않을까?
  • 결제 대행사 API를 경유하지 않고, 프로젝트 내에서 가상의 point로 결제를 구현한다면 어떻게 해당 내용을 구현할 수 있을까?

4) 주문

자사 제품을 주문하는 절차를 웹 서비스 내에서 기술적으로 구현.

  • 자사 제품에 대한 구매 욕구가 주문까지 이어질 수 있게 개발자가 기술적으로 구현해야할 요소는 무엇인가?
  • 대부분의 사이트가 보유하고 있는 장바구니 기능은 필수일까 아니면 선택일까?
  • 장바구니에 개별 품목을 선택하여 삭제할 수 있는 부분은 왜 존재하는 것이고, 기술적으로 어떻게 구현할 수 있을까?
  • 주문의 절차가 까다롭게 되면 우리의 매출 수익성에 어떠한 영향을 줄까?
  • 해당 사이트는 개별 상품에 대한 구매가 가능한가? 아니면 전체 보수의 상품에 대한 구매가 가능한가?

💽 WorkFlow & ERD


-> 워크 플로우는 메인페이지에 로그인이 되어 있는지 유무를 확인한뒤 회원으로 가입되어 로그인이 되어있으면 구독 서비스를 이용할수 있도록 하였고, 만약에 Dobby 회원으로 가입되어 있지 않다면? 새로 회원가입을 해서 로그인 후에 서비스를 이용할수 있도록 구성했고, 반대로 회원으로 가입되어 있지만 로그인을 안했을땐? 로그인 페이지로 이동해서 로그인후에 서비스(상품결제, 장바구니)를 이용할 수 있도록 플로우를 구성하였습니다.


-> ERD는 총 9가지 순으로 구성되어 있으며 Dobby 서비스를 이용하는 회원이 새로 회원가입하면 users의 정보가 DataBase에 저장되고 로그인 했을때 comments, products, cart와 연결되어 각각의 기능들에게 적용될수 있도록 ERD를 구성하였습니다.

✅ 구현 기능 선정

1) 필수 구현 List


  1. 메인 (Main)
  2. 로그인/회원가입 (Login/Signup)
  3. 상품 리스트 (Product List)
  4. 상품 상세 (Product Details)
  5. 장바구니 (Shopping Basket)
  6. 결제(Payment)

2) 추가 구현 List

  1. ID 찾기(문자 인증번호 사용)/PW 찾기(이메일 인증번호 사용 & 비밀번호 초기화)
  2. 상품 검색 기능, 기준별 상품 정렬

3) Front-End & Back-End 기능 구현 파트

Front-End

  • 홍지영 : 장바구니 페이지, 결제 페이지
  • 김성호 : 로그인 & 회원가입 페이지, 아이디(ID) & 비밀번호(PW) 찾기 페이지
  • 박주희 : 메인 페이지, 전체 상품 페이지
  • 최민지 : 상품 상세(제품 정보, 리뷰), NavBar, 배송지 페이지

Back-End

  • 조민수 : 장바구니 페이지, 전체 상품 페이지
  • 유진서 : 로그인 & 회원가입 페이지, 아이디(ID) & 비밀번호(PW) 찾기 페이지, 메인 페이지
  • 김영범 : 상품 상세 페이지
  • 최현수 : 결제 페이지

4) 기능 정의서

1) 로그인 -> 유저

  • 요구사항
  1. email과 Password를 이용해서 가입자 여부를 확인할수 있어야 합니다.
  2. 서비스 이용 토큰 발급 여부가 가능해야 합니다.
  • 필수 데이터 : email, password
  • 예외 처리
  1. email, password 유효성 검사시 버튼 비활성화, 활성화 여부가 확인 되어야 합니다.
  2. 쿠키에 토큰(token)을 담아서 발행해야 합니다.
  3. 로그인 성공시 nicknametoken 을 발행해야 합니다.

2) 회원가입 -> 유저

  • 요구사항 : 회원 이용 약관을 모두 동의했을때 회원가입이 성공할수 있도록 해야합니다.
  • 필수 데이터 : email, password, name, nickname, phonenumber, 약관
  • 예외 처리
  1. email에는 .@ 이 필수 포함되어야 합니다.
  2. 비밀번호는 10자리 이상 필요합니다.(추가. 비밀번호 확인)
  3. name은 유효성 검사가 필요합니다.

3) 로그아웃 -> 유저

  • 요구사항
  1. 쿠키에 담겨있는 Token이 삭제되어야 합니다.
  2. 로그아웃을 눌렀을때 Main 페이지로 이동해야 합니다.
  • 필수 데이터 : 쿠키에 담겨있는 Token

4) 메인 구독 -> 메인

  • 요구사항 : 사용자가 DIY 구독제를 선택하여 바로 결제할수 있도록 결제 페이지로 이동할수 있도록 구성해주세요.

5) 메인 스토어 -> 메인

  • 요구사항
  1. 카테고리 3개(전체 창작적, 수집성), 스와이퍼(슬라이더) 3개 구성해야합니다.
  2. 스와이퍼(신상품 , Best, MD 추천)
  3. 스와이퍼는 좌우로 클릭이 가능해야 합니다.
  4. ICON 이미지 클릭시 리스트 페이지로 이동합니다.

6) 제품 리스트 페이지 -> 제품

  • 요구사항
  1. 메인 카테고리 ICON 클릭시 제품 리스트 페이지로 이동합니다.
  2. 창작적(리빙, 패브릭,푸드 DIY) & 수집성(캐릭터 & 인형, 스티커 & 메모지)
  3. 제품 클릭시 제품 상세 페이지로 이동합니다.

7) 제품 상세 페이지 -> 제품

  • 요구사항
  1. 클릭한 제품의 상세 페이지를 볼수있도록 해야 합니다.
  2. 원하는 수량을 장바구니에 담을 수 있습니다.
  3. 후기를 확인할수 있어야 합니다.

8) 장바구니 페이지 -> 제품

  • 요구사항
  1. 장바구니에 담은 제품의 목록을 확인할수 있습니다.
  2. 장바구니에 담긴 제품의 수량을 수정할수 있습니다.(추가 & 삭제)

9) 결제 페이지 -> 제품

  • 요구사항
  1. 선택한 제품을 포인트로 결제할수 있어야 합니다.
  2. 배송지 입력이 가능해야 합니다.
  3. 배송지 여러개 일수 있어서 List 페이지를 만들어야 합니다.
  4. 장바구니에 담은 결제할 상품 List를 보여줄수 있어야 합니다.(사진 포함)

✅ 맡은 역할(페이지 & 기능)

상세역할 : 프로덕트 개발 담당
로그인 : 입력한 회원 정보를 Back-End API로 전송하여 로그인 여부 확인
회원가입 : 회원 이용 약관 동의 시 회원가입 버튼 활성화 및 중복 가입 방지 기능 구현
아이디 찾기 : 휴대폰 인증번호 입력 후 매칭되는 ID를 알림창에 표시
비밀번호 찾기 : 이메일 인증번호 입력 후 새로운 비밀번호 DataBase 저장

1) 로그인/회원가입(UI & Func)

  • (1) 로그인 : 이메일과 비밀번호 10자리 이상 입력했을때 input 유효성 검사로 로그인 버튼 활성화 여부 기능과, 로그인시 Token과 Nickname을 localstroge에 Token, Nickname 정보를 저장하고, 회원 정보가 저장된 Back-End API 서버와 통신하여 로그인 버튼을 눌렀을때 로그인 성공/실패 여부의 기능을 구현하였습니다.
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './Login.scss';

const Login = () => {
  // ID(이메일)
  const [id, setID] = useState('');
  const saveUserID = event => {
    setID(event.target.value);
  };
  // PW(비밀번호)
  const [pw, setPW] = useState('');
  const saveUserPW = event => {
    setPW(event.target.value);
  };
  // 로그인 버튼
  const isInvalid = id.includes('@', '.') && pw.length >= 10;
  // 회원가입 버튼(회원가입 페이지로 이동)
  const navigate = useNavigate();
  const goToSignup = () => {
    navigate('/signup');
  };
  const goToFindID = () => {
    navigate('/findid');
  };
  const goToFindPW = () => {
    navigate('/findpw');
  };

  const goToMain = () => {
    fetch('http://10.58.52.121:8000/users/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({
        email: id,
        password: pw,
      }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'LOG_IN_SUCCESS') {
          alert('로그인 되었습니다.');
          localStorage.setItem('nickname', data.nickname);
          localStorage.setItem('token', data.token);
          navigate('/main?dobbyBox=basic');
        } else if (data.message === 'INVALID EMAIL OR PASSWORD') {
          alert('가입되지 않은 정보입니다.');
        }
      });
  };
  return (
    <div className="mainLoginBody">
      <h1 className="helloText">안녕하세요!😊</h1>
      <p className="intoText">즐거움으로 찾아오는 인생취미, Dobby 입니다.</p>
      <div className="loginTextFrame">
        <h2 className="loginText">LOGIN</h2>
      </div>
      <div className="inputFrame">
        <input
          className="idInput"
          type="text"
          onChange={saveUserID}
          placeholder="이메일"
        />
        <input
          className="pwInput"
          type="password"
          onChange={saveUserPW}
          placeholder="비밀번호"
        />
        <button
          className={isInvalid ? 'loginButton' : 'disabledButton'}
          disabled={isInvalid ? false : true}
          onClick={goToMain}
        >
          로그인
        </button>
      </div>
      <div className="idpwButtonFrame">
        <button className="idButton" onClick={goToFindID}>
          아이디 찾기
        </button>
        <button className="pwButton" onClick={goToFindPW}>
          비밀번호 찾기
        </button>
      </div>
      <div className="signupButtonFrame">
        <button className="signupButton" onClick={goToSignup}>
          회원가입
        </button>
      </div>
    </div>
  );
};
export default Login;
  • 로그인 UI : 회원가입 및 로그인이 가능하다는 것을 로그인 페이지로 이동하자마자 곧바로 버튼을 보여 줘서 미가입 유저의 회원가입 장벽을 술담화보다 조금 더 낮췄습니다.

  • 이메일, 비밀번호를 입력하지 않았을때 로그인 버튼 비활성화 상태 (로그인 불가능)

  • 이메일, 비밀번호를 입력했을때 로그인 버튼 활성화 (로그인 가능)

  • (2) 회원가입 : 정규 표현식을 사용하여 이메일, 비밀번호, 닉네임, 전화번호, 이름에 맞게 회원정보를 작성할수 있도록 유효성 검사 및 오류 메세지 표시기능과, fetch 메서드를 이용하여 회원 이용 약관에 모두 동의했을때 회원가입 버튼 활성화 여부 및 회원가입 성공/실패(중복가입방지) 기능을 구현하였습니다.

import React, { useEffect, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import './Signup.scss';

const Signup = () => {
  // 이메일, 비밀번호, 비밀번호 확인, 이름, 닉네임 확인
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [passwordConfir, setPasswordConfir] = useState('');
  const [name, setName] = useState('');
  const [phonenumber, setPhoneNumber] = useState('');
  const [nickname, setNickName] = useState('');
  
  // (이메일, 비밀번호, 비밀번호 확인, 이름, 닉네임) 오류 메세지 상태 저장
  const [emailMessage, setEmailMessage] = useState('');
  const [passwordMessage, setPasswordMessage] = useState('');
  const [passwordConfirMessage, setPasswordConfirMessage] = useState('');
  const [nameMessage, setNameMessage] = useState('');
  const [phonenumberMessage, setPhonenumberMessage] = useState('');
  const [nicknameMessage, setNickNameMessage] = useState('');
  
  // (이메일, 비밀번호, 비밀번호 확인, 이름, 닉네임) 입력창 유효성 검사
  const [isEmail, setIsEmail] = useState(false);
  const [isPassword, setIsPassword] = useState(false);
  const [isPasswordConfir, setIsPasswordConfir] = useState(false);
  const [isName, setIsName] = useState(false);
  const [isPhonenumber, setIsPhonenumber] = useState(false);
  const [isNickName, setIsNickName] = useState(false);
  
  // 체크박스(state)
  const [allCheck, setAllcheck] = useState(false);
  const [useCheck, setUsecheck] = useState(false);
  const [infoCheck, setInfocheck] = useState(false);
  
  // 이메일
  const onChangeEmail = useCallback(event => {
    const emailRagex =
      /([\w-.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
    const emailCurrent = event.target.value;
    setEmail(emailCurrent);
    if (!emailRagex.test(emailCurrent)) {
      setEmailMessage('올바른 이메일 형식이 아닙니다.');
      setIsEmail(false);
    } else {
      setEmailMessage('올바른 이메일 형식입니다.');
      setIsEmail(true);
    }
  }, []);
  // 비밀번호
  const onChangePassword = useCallback(event => {
    const passwordRagex = /^(?=.*[a-zA-Z])(?=.*\d).{9,}$/;
    const passwordCurrent = event.target.value;
    setPassword(passwordCurrent);
    if (!passwordRagex.test(passwordCurrent)) {
      setPasswordMessage('영문과 숫자를 혼합해서 10자리 이상 입력해주세요.');
      setIsPassword(false);
    } else {
      setPasswordMessage('안전한 비밀번호 입니다.');
      setIsPassword(true);
    }
  }, []);
  // 비밀번호 확인
  const onChangePasswordConfir = useCallback(
    event => {
      const passwordConfirCurrent = event.target.value;
      setPasswordConfir(passwordConfirCurrent);
      if (password === passwordConfirCurrent) {
        setPasswordConfirMessage('비밀번호가 일치합니다.');
        setIsPasswordConfir(true);
      } else {
        setPasswordConfirMessage(
          '비밀번호가 일치하지 않습니다. 다시한번 확인해주세요',
        );
        setIsPasswordConfir(false);
      }
    },
    [password],
  );
  // 이름
  const onChangeName = useCallback(event => {
    const nameRagex = /^[ㄱ-ㅎ|가-힣]+$/;
    const nameCurrent = event.target.value;
    setName(nameCurrent);
    if (!nameRagex.test(nameCurrent)) {
      setNameMessage('올바른 이름 형식이 아닙니다.');
      setIsName(false);
    } else {
      setNameMessage('올바른 이름 형식입니다.');
      setIsName(true);
    }
  }, []);
  // 전화번호
  const onChangePhoneNumber = useCallback(event => {
    const phoneNumberRagex = /^(01[016789]{1})[0-9]{3,4}[0-9]{4}$/;
    const phoneNumberCurrent = event.target.value;
    setPhoneNumber(phoneNumberCurrent);
    if (!phoneNumberRagex.test(phoneNumberCurrent)) {
      setPhonenumberMessage('전화번호를 올바르게 입력해주세요');
      setIsPhonenumber(false);
    } else {
      setPhonenumberMessage('정확한 전화번호 입니다.');
      setIsPhonenumber(true);
    }
  }, []);
  // 닉네임
  const onChangeNickName = useCallback(event => {
    const nicknameRagex = /^[a-z|A-Z]+$/;
    const nicknameCurrent = event.target.value;
    setNickName(nicknameCurrent);
    if (!nicknameRagex.test(nicknameCurrent)) {
      setNickNameMessage('닉네임은 영어로 입력해주세요');
      setIsNickName(false);
    } else {
      setNickNameMessage('올바른 닉네임 형식입니다.');
      setIsNickName(true);
    }
  }, []);
  // 모두 동의 버튼(전체 체크박스를 클릭했을때)
  const inValid = !allCheck;
  const allBtnevent = () => {
    if (allCheck === false) {
      setAllcheck(true);
      setUsecheck(true);
      setInfocheck(true);
    } else {
      setAllcheck(false);
      setUsecheck(false);
      setInfocheck(false);
    }
  };
  // 이용약관 동의 버튼
  const useBtnevent = () => {
    if (useCheck === false) {
      setUsecheck(true);
    } else {
      setUsecheck(false);
    }
  };
  // 개인정보 수집 버튼
  const infoBtnevent = () => {
    if (infoCheck === false) {
      setInfocheck(true);
    } else {
      setInfocheck(false);
    }
  };
  // 2개 버튼 모두체크시 전체동의 자동체크
  useEffect(() => {
    if (useCheck === true && infoCheck === true) {
      setAllcheck(true);
    } else {
      setAllcheck(false);
    }
  }, [useCheck, infoCheck]);
  // 로그인 페이지 이동
  const navigate = useNavigate();
  const goToLogin = () => {
    navigate('/');
  };
  // 회원가입 정보 입력후 회원가입 하기 버튼
  const goToSignup = () => {
    fetch('http://10.58.52.121:8000/users/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({
        email: email,
        password: password,
        name: name,
        phonenumber: phonenumber,
        nickname: nickname,
      }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'SIGN_UP_SUCCESS') {
          alert('회원가입이 완료 되었습니다.');
          goToLogin('/');
        } else {
          alert('이미 가입된 사용자 입니다.');
        }
        console.log(data);
      });
  };
  return (
    <div className="signupBody">
      <header className="headerFrame">
        <h1 className="headerText">환영합니다!</h1>
        <p className="subText">지금 회원가입하면 최대 100 POINT를 드려요</p>
      </header>
      <form className="inputFrame">
        <div className="formbox">
          <input
            className="userInput"
            type="text"
            value={email}
            onChange={onChangeEmail}
            placeholder="이메일을 입력해주세요"
          />
          {email.length > 0 && (
            <span className={`message ${isEmail ? 'success' : 'error'}`}>
              {emailMessage}
            </span>
          )}
        </div>
        <div className="formbox">
          <input
            className="userInput"
            type="password"
            value={password}
            onChange={onChangePassword}
            placeholder="비밀번호를 입력해주세요"
            maxLength={10}
          />
          {password.length > 0 && (
            <span className={`message ${isPassword ? 'success' : 'error'}`}>
              {passwordMessage}
            </span>
          )}
        </div>
        <div className="formbox">
          <input
            className="userInput"
            type="password"
            onChange={onChangePasswordConfir}
            placeholder="비밀번호를 다시한번 입력해주세요"
            maxLength={10}
          />
          {passwordConfir.length > 0 && (
            <span
              className={`message ${isPasswordConfir ? 'success' : 'error'}`}
            >
              {passwordConfirMessage}
            </span>
          )}
        </div>
        <div className="formbox">
          <input
            className="userInput"
            type="text"
            value={name}
            onChange={onChangeName}
            placeholder="이름을 입력해주세요"
          />
          {name.length > 0 && (
            <span className={`message ${isName ? 'success' : 'error'}`}>
              {nameMessage}
            </span>
          )}
        </div>
        <div className="formbox">
          <input
            className="userInput"
            type="phonenumber"
            value={phonenumber}
            onChange={onChangePhoneNumber}
            placeholder="전화번호를 입력해주세요"
            maxLength={13}
          />
          {phonenumber.length > 0 && (
            <span className={`message ${isPhonenumber ? 'success' : 'error'}`}>
              {phonenumberMessage}
            </span>
          )}
        </div>
        <div className="formbox">
          <input
            className="userInput"
            type="text"
            value={nickname}
            onChange={onChangeNickName}
            placeholder="닉네임을 입력해주세요"
          />
          {nickname.length > 0 && (
            <span className={`message ${isNickName ? 'success' : 'error'}`}>
              {nicknameMessage}
            </span>
          )}
        </div>
      </form>
      <p className="coditionsText">Dobby 서비스 이용약관에 동의해주세요.</p>
      <div className="checkFormBox">
        <span className="checkboxFrame">
          <input
            className="allCheck"
            type="checkbox"
            checked={allCheck}
            onChange={allBtnevent}
          />
          <p className="checkText">모두 동의합니다</p>
        </span>
        <span className="checkboxFrame">
          <input
            className="termsCheck"
            type="checkbox"
            checked={useCheck}
            onChange={useBtnevent}
          />
          <p className="checkText">(필수) 이용 약관 동의</p>
        </span>
        <span className="checkboxFrame">
          <input
            className="personalCheck"
            type="checkbox"
            checked={infoCheck}
            onChange={infoBtnevent}
          />
          <p className="checkText">(필수) 개인정보 수집 및 이용 동의</p>
        </span>
      </div>
      <div>
        <button
          className={inValid ? 'disableButton' : 'signupButton'}
          onClick={goToSignup}
        >
          회원가입 하기
        </button>
      </div>
    </div>
  );
};

export default Signup;
  • 회원가입 UI : 상단에 신규 회원에게 가입을 환영하는 인삿말과 포인트 지급으로 이벤트성 안내를 표시해 신규 회원으로 가입할수 있도록 유도했고, 일반 신규가입 보다 간단하게 신규회원의 정보를 간편하게 입력할수 있도록 만들었고, 모두 동의합니다를 체크했을때 회원가입이 이루어 지도록 간소화 했습니다.

  • 회원정보 및 서비스 이용약관 모두 동의로 체크 비활성화시, 회원가입 버튼 비활성화 상태

  • 회원정보 및 서비스 이용약관 모두 동의로 체크 활성화시, 회원가입 버튼 활성화 상태

2) 아이디/비밀번호 찾기(UI & Func)

  • (3)아이디 찾기 : 정규식과 fetch 메서드를 사용하여 휴대폰 번호를 입력후 인증번호 받기 버튼을 누르면 Back-End API에 저장된 회원정보를 요청해서 인증번호를 고객 휴대폰 문자로 전송하고, 전송 받은 인증번호를 입력하고 데이터에 저장된 email(ID)을 알림창으로 보여주는 기능을 구현하였습니다.
import React, { useState } from 'react';
import './FindID.scss';

const FindID = () => {
  // 휴대폰 번호 입력시
  const [phoneNumber, setPhoneNumber] = useState('');
  // 인증번호 입력시
  const [number, setNumber] = useState('');
  // 휴대폰 유효성 검사
  const [phoneNumberMessage, setPhoneNumberMessage] = useState('');
  // 인증번호 유효성 검사
  const [numberMessage, setNumberMessage] = useState('');
  // 휴대폰 오류 메세지
  const [isPhoneNumber, setIsPhoneNumber] = useState(false);
  // 인증번호 오류 메세지
  const [isNumber, setIsNumber] = useState(false);

  // 전화번호 조건식
  const onChangePhoneNumber = event => {
    const phoneNumberRagex = /^01(?:0|1|[6-9])(?:\d{3}|\d{4})\d{4}$/;
    const phoneNumberCurrent = event.target.value;
    setPhoneNumber(phoneNumberCurrent);
    if (!phoneNumberRagex.test(phoneNumberCurrent)) {
      setPhoneNumberMessage('입력한 전화번호를 다시한번 확인해주세요');
      setIsPhoneNumber(false);
    } else {
      setPhoneNumberMessage('확인되었습니다.');
      setIsPhoneNumber(true);
    }
  };
  // 인증번호 조건식
  const onChangeVerifinNumber = event => {
    const numberRagex = /^[0-9]+$/;
    const numberCurrent = event.target.value;
    setNumber(numberCurrent);
    if (!numberRagex.test(numberCurrent)) {
      setNumberMessage('전송받은 인증번호를 다시한번 확인해주세요');
      setIsNumber(false);
    } else {
      setNumberMessage('확인되었습니다.');
      setIsNumber(true);
    }
  };
  // 이름과 휴대폰 번호 입력값이 모두 비어있지 않을때 버튼 활성화
  const phoneinValid = phoneNumber.length !== 11;
  const verifinValid = number.length !== 6;
  // 인증번호 받기 버튼
  const handleVerifin = () => {
    fetch('http://10.58.52.121:8000/users/phoneauth', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({
        phoneNumber: phoneNumber,
      }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'AUTHENTICATION_NUMBER_SUCCESS') {
          alert('문자로 인증번호를 전송했습니다. 확인해주세요');
        } else {
          alert('아이디를 찾을 수 없습니다.');
        }
      });
  };
  // 아이디 찾기 버튼
  const handleIDFind = () => {
    fetch('http://10.58.52.121:8000/users/phoneauth/phoneverifynumber', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({
        phoneNumber: phoneNumber,
        number: number,
      }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'FIND_ID_SUCCESS') {
          alert(`회원님의 아이디는 ${data.email} 입니다.`);
        } else {
          alert('아이디를 찾을 수 없습니다.');
        }
      });
  };
  return (
    <div className="idFindFrame">
      <h1 className="idText">아이디 찾기</h1>
      <form className="findIDFrame">
        <input
          className="userInput"
          type="phomenumber"
          maxLength={11}
          onChange={onChangePhoneNumber}
          placeholder="휴대폰 번호를 입력해주세요 (- 제외)"
        />
        {phoneNumber.length > 0 && (
          <span className={`message ${isPhoneNumber ? 'success' : 'error'}`}>
            {phoneNumberMessage}
          </span>
        )}
      </form>
      <form className="findIDFrame">
        <input
          className="userInput"
          type="text"
          maxLength={6}
          onChange={onChangeVerifinNumber}
          placeholder="인증번호를 입력해주세요"
        />
        {number.length > 0 && (
          <span className={`message ${number ? 'success' : 'error'}`}>
            {numberMessage}
          </span>
        )}
      </form>
      <div className="phoneButtonFrame">
        <button
          className={phoneinValid ? 'disabledButton' : 'findIDButton'}
          disabled={phoneinValid}
          onClick={handleVerifin}
        >
          인증번호 받기
        </button>
        <div className="verifinButtonFrame">
          <button
            className={verifinValid ? 'disabledButton' : 'findIDButton'}
            disabled={verifinValid}
            onClick={handleIDFind}
          >
            아이디 찾기
          </button>
        </div>
      </div>
    </div>
  );
};

export default FindID;
  • 아이디 찾기 UI : 아이디 찾기는 휴대폰 번호를 입력하고 인증번호를 문자로 받아서 인증번호를 입력하면 이메일 data를 받아와 user의 이메일을 알림창에 보여주는 방식으로 구현하였습니다.

  • 휴대폰 번호 미입력시 인증번호 받기 버튼 비활성화 상태
  • 인증번호 미입력시 아이디 찾기 버튼 비활성화 상태

  • 휴대폰 번호 입력후 인증번호를 받으면 가입 유저의 휴대폰 번호로 인증번호 6자리 전송

  • 휴대폰 인증번호 확인후 입력창에 인증번호 6자리 입력시 아이디 찾기 버튼 활성화
    (버튼을 누르면 alert(알림창)로 회원의 아이디(이메일)을 보여줍니다.)

  • (4) 비밀번호 찾기 : fetch 메서드를 사용하여 이메일을 입력후 본인 인증하기 버튼을 누르면, 이메일 인증번호 전송 여부 성공/실패 기능과 이메일로 인증번호 6자리를 받고 새로운 비밀번호를 입력하면, 이전의 비밀번호는 초기화 되고 새로운 비밀번호가 회원 정보로 새로 DataBase에 등록되어 로그인 할때 적용되는 기능을 구현하였습니다.

import React, { useState } from 'react';
import './FindPW.scss';
import { useNavigate } from 'react-router-dom';

const FindPW = () => {
  // 이메일 입력시
  const [email, setEmail] = useState('');
  
  // 인증번호, 새 비밀번호, 새 비밀번호 확인 입력시
  const [number, setNumber] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [newpasswordConfir, setNewPasswordConfir] = useState('');
  
  // 이메일 유효성 검사
  const [emailMessage, setEmailMessage] = useState('');
  
  // 인증번호, 새 비밀번호, 새 비밀번호 확인 유효성 검사
  const [numberMessage, setNumberMessage] = useState('');
  const [newpasswordMessage, setNewPasswordMessage] = useState('');
  const [newpasswordConfirMessage, setNewPasswordConfirMessage] = useState('');
  
  // 이메일 오류 메세지
  const [isEmail, setIsEmail] = useState(false);
  
  // 인증번호, 새 비밀번호, 새 비밀번호 확인 오류 메세지
  const [isNumber, setIsNumber] = useState(false);
  const [isNewPassword, setIsNewPassword] = useState(false);
  const [isNewPasswordConfir, setIsNewPasswordConfir] = useState(false);
  
  // 이메일 조건식
  const onChangeEmail = event => {
    const emailRagex =
      /([\w-.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
    const emailCurrent = event.target.value;
    setEmail(emailCurrent);
    if (!emailRagex.test(emailCurrent)) {
      setEmailMessage('올바른 이메일 형식이 아닙니다.');
      setIsEmail(false);
    } else {
      setEmailMessage('올바르게 입력하셨습니다.');
      setIsEmail(true);
    }
  };
  // 인증번호 조건식
  const onChangeNumber = event => {
    const numberRagex = /^[0-9]+$/;
    const numberCurrent = event.target.value;
    setNumber(numberCurrent);
    if (!numberRagex.test(numberCurrent)) {
      setNumberMessage('전송받은 인증번호를 다시한번 확인해주세요');
      setIsNumber(false);
    } else {
      setNumberMessage('확인되었습니다.');
      setIsNumber(true);
    }
  };
  // 새 비밀번호 조건식
  const onChangeNewPassword = event => {
    const newPasswordRagex = /^(?=.*[a-zA-Z])(?=.*\d).{9,}$/;
    const passwordCurrent = event.target.value;
    setNewPassword(passwordCurrent);
    if (!newPasswordRagex.test(passwordCurrent)) {
      setNewPasswordMessage('영문+숫자 조합으로 10자리 이상 입력해주세요.');
      setIsNewPassword(false);
    } else {
      setNewPasswordMessage('안전한 비밀번호 입니다.');
      setIsNewPassword(true);
    }
  };
  // 새 비밀번호 확인 조건식
  const onChangeNewPasswordConfir = event => {
    const newPasswordConfirCurrent = event.target.value;
    setNewPasswordConfir(newPasswordConfirCurrent);
    if (newPassword === newPasswordConfirCurrent) {
      setNewPasswordConfirMessage('비밀번호가 일치합니다.');
      setIsNewPasswordConfir(true);
    } else {
      setNewPasswordConfirMessage(
        '비밀번호가 일치하지 않습니다. 다시한번 확인해주세요',
      );
      setIsNewPasswordConfir(false);
    }
  };

  // 이메일 예외처리
  const emailIsvalid = email.includes('@') && email.includes('.');
  // 새 비밀번호, 새 비밀번호 확인 예외 처리
  const newPasswordIsvalid =
    number.length && newPassword.length >= 8 && newpasswordConfir.length >= 8;
  const navigate = useNavigate();
  const goToLogin = () => {
    navigate('/');
  };
  // 인증받기 버튼
  const handleCertificate = () => {
    fetch('http://10.58.52.121:8000/users/emailauth', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({
        email: email,
      }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'AUTHENTICATION_MAIL_SUCCESS') {
          alert('이메일로 인증번호를 전송했습니다. 확인해주세요');
        } else {
          alert('아이디를 찾을 수 없습니다.');
        }
      });
  };
  // 새 비밀번호 등록하기 버튼
  const handleNewPassword = () => {
    fetch('http://10.58.52.121:8000/users/emailauth/emailverifynumber', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({ email, number, newPassword }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.message === 'AUTHENTICATION_PASSWORD_SUCCESS') {
          alert('비밀번호 등록이 완료되었습니다.');
          goToLogin('/');
        } else {
          alert('비밀번호를 다시한번 확인해주세요');
        }
      });
  };
  return (
    <div className="findPWFrame">
      <h1 className="findpwText">비밀번호 찾기</h1>
      <form className="formPWFrame">
        <div>
          <input
            className="emailInput"
            type="text"
            onChange={onChangeEmail}
            placeholder="이메일을 입력해주세요"
          />
          {email.length > 0 && (
            <span className={`message ${isEmail ? 'success' : 'error'}`}>
              {emailMessage}
            </span>
          )}
        </div>
      </form>
      <div className="confirButtonFrame">
        <button
          className={emailIsvalid ? 'confirButton' : 'disabledButton'}
          disabled={emailIsvalid ? false : true}
          onClick={handleCertificate}
        >
          본인 인증하기
        </button>
      </div>
      <form className="formCertFrame">
        <div>
          <input
            className="userInput"
            type="text"
            onChange={onChangeNumber}
            maxLength={6}
            placeholder="인증번호를 입력해주세요"
          />
          {number.length > 0 && (
            <span className={`message ${isNumber ? 'success' : 'error'}`}>
              {numberMessage}
            </span>
          )}
        </div>
      </form>
      <form className="formCertFrame">
        <div>
          <input
            className="userInput"
            type="password"
            onChange={onChangeNewPassword}
            placeholder="새 비밀번호를 입력해주세요"
          />
          {newPassword.length > 0 && (
            <span className={`message ${isNewPassword ? 'success' : 'error'}`}>
              {newpasswordMessage}
            </span>
          )}
        </div>
      </form>
      <form className="formCertFrame">
        <div>
          <input
            className="userInput"
            type="password"
            onChange={onChangeNewPasswordConfir}
            placeholder="새 비밀번호를 다시한번 입력해주세요"
          />
          {newpasswordConfir.length > 0 && (
            <span
              className={`message ${isNewPasswordConfir ? 'success' : 'error'}`}
            >
              {newpasswordConfirMessage}
            </span>
          )}
        </div>
      </form>
      <div className="confirButtonFrame">
        <button
          className={newPasswordIsvalid ? 'confirButton' : 'disabledButton'}
          disabled={newPasswordIsvalid ? false : true}
          onClick={handleNewPassword}
        >
          새 비밀번호 등록하기
        </button>
      </div>
    </div>
  );
};

export default FindPW;
  • 비밀번호 찾기 UI : 회원으로 가입되어 있지만 비밀번호를 잊어버려서 로그인 할수 없을경우 사용하는 이메일을 입력후 이메일로 전송되는 인증번호를 받아서 인증번호를 입력후 새로운 비밀번호를 입력하면 백엔드 데이터베이스에 저장되고 이전 비밀번호는 데이터베이스에서 초기화 할수 있도록 하였습니다.

  • 유저의 이메일 미입력시 본인 인증하기 버튼 비활성화

  • 인증하기 버튼 클릭후 본인의 이메일로 인증번호 전송

  • 다음과 같이 인증번호를 입력후 새로운 비밀번호, 비밀번호 확인창에 입력하면 새 비밀번호 등록하기 버튼 활성화

✅ 팀 프로젝트간 협업의 중요성

  • 내가 있었던 B팀의 팀원들과 주로 소통하는데 사용했던 Slack 채널방
  • Front-End & Back-End간의 프로젝트 상태들을 관리하는 Trello
  • 각자가 맡은 티켓 상황들을 매일매일 공유하는 Team Meeting(Daily Standup Meeting)

🚨 기술적 문제 & 개선 성과

[기술 문제]

  • 글자 형식으로 된 버튼은 UI 시인성이 낮아, 사용자들이 버튼인지 Text인지 인식하기 어렵고, 시력이 좋지 않은 사용자의 경우에는 시각적인 피로감이 증가하여 서비스 이용에 불편함을 겪을 수 있기 때문에, 이에 따른 글자 형식의 아이디 & 비밀번호 찾기 버튼을 개선해야 할 문제가 있었습니다.

[고민]

  • 이로써 사용자의 이탈이 언제든 쉽게 이루어진다는 것을 근거로 사용자들의 혼란을 줄이고 편의성을 높이기 위해 버튼의 User Interface를 보완시킬 필요성을 느끼게 되었습니다.

[시도 방법]

  • 글자 형식으로 된 아이디 & 비밀번호 찾기 버튼을 색깔별로 구분해서 쉽게 인식할수 있도록 UI를 변경하여 사용자들이 쉽게 파악할 수 있도록 하는 방법으로 보완하였습니다.

[개선 성과]

  • 사용자들이 로그인 페이지 내에서 아이디 & 비밀번호 찾기 버튼을 쉽게 찾을 수 있도록 개선함과 동시에 사용자 이탈을 방지하고 편의성을 증가시킬수 있었습니다.

😊 2차 프로젝트 후기

긍정적인 점

  • 처음 해보는 이메일(ID) 인증, 비밀번호 초기화 및 새로운 비밀번호 생성 하는 기능을 직접 구현해보는 것이 재밌었습니다.
  • Slack과 Trello로 이슈들을 팀원들이 적극적으로 공유하고, 문제가 발생하거나 어려움이 있었을때 같이 도와주면서 기간안에 잘 마무리 할 수 있었습니다.

배웠던 점

  • 별도로 React Test 코드를 만들어서 직접적으로 구현하고자 하는 기능들이 잘 동작하는지 확인함과 직접 프로젝트에 적용시키면서 어떻게 하면 효율적으로 개발에 집중할 수 있는지 경험할 수 있었습니다.

아쉬웠던 부분

  • 기능 구현할때 State를 너무 많이 써서 아직 React의 전반적인 이해도가 부족했음을 느꼈습니다.
  • 2주라는 짧은 기간동안 다양한 기능들을 만들어 보기에 부족했고, 직접 짠 코드를 리팩토링 하지 못했던 점이 아쉬웠습니다.

✅ 2차 프로젝트 결과물

Mac UI/UX Template Design)

IPad UI/UX Template Design)

profile
안녕하세요 Junior UIUX Designer 입니다 😊

0개의 댓글