not-a-gardener 개발기 2) 계정 찾기, 최근 소셜 로그인

메밀·2023년 4월 25일
0

not-a-gardener

목록 보기
2/13

1. 아이디/비밀번호 찾기 구현

대략적인 흐름은 다음과 같다.
흐름

2. 구현 내용

1) 아이디/비밀번호 찾기

ForgotAccount.jsx는 단순히 아이디를 찾을 것인지 비밀번호를 찾을 것인지 확인하는 페이지이므로 생략한다.

2) 이메일 인증과 인증코드 발송

ValidateGardener.jsx 또한 (컨텐츠가 바뀌지 않는) 디자인 페이지이므로 생략한다.

— VerifyAccountContent.jsx

이메일과 이메일로 전송한 인증코드를 통하여 유저를 인증하는 페이지다.

아이디 찾기를 눌렀든 비밀번호 찾기를 눌렀든 이 페이지로 이동한다.

const VerifyAccountContent = ({successContent, setEmail, setGardenerList}) => {
  // 이메일 input
  const [input, setInput] = useState("");
  // 백엔드에서 유저의 메일로 인증 코드를 보낼 동안 대기해달라는 알림을 띄워놓기 위해
  const [isWaiting, setIsWaiting] = useState(false);
  // 제출한 이메일이 유효하지 않을 시 에러 메시지
  const [errorMsg, setErrorMsg] = useState("");
  // 유저의 이메일로 보낸 인증코드를 저장
  const [identificationCode, setIdentification] = useState("");
  // 유저에게 입력받은 인증코드를 저장
  const [inputIdCode, setInputIdCode] = useState("");

  // 인증 단계를 저장
  // 1. pending: 인증코드로 인증 이전
  // 2. fulfilled: 인증 완료
  // 3. rejected: 인증 실패
  const [identify, setIdentify] = useState("pending");

  // 이메일 입력 onChange 함수
  const onChange = (e) => {
    setInput(e.target.value);

    if (errorMsg !== "") {
      setErrorMsg("");
    }
  }

  // 백엔드로 이메일 제출
  const submitEmail = async () => {
    setIsWaiting(true);

    try {
      const res = await axios.get(`/gardener/email/${input}`);
      // console.log("res.data", res.data);

      setIdentification(res.data.identificationCode); // 인증코드
      setEmail(res.data.email); // 유저 이메일
      setGardenerList(res.data.gardeners); // 회원목록
    } catch (e) {
      // 이메일 인증 실패
      setErrorMsg(e.response.data.errorDescription);
    }

    setIsWaiting(false);
  }

  // 피드백 메시지 함수
  const getFeedbackMsg = () => {
    if (errorMsg !== "") {
      return errorMsg;
    } else if (input !== "" && !verifyEmail(input)) {
      return "이메일 형식을 확인해주세요";
    } else if (isWaiting) {
      return "잠시만 기다려주세요";
    } else if (identificationCode !== "") {
      return "본인 확인 이메일이 발송되었습니다."
    } else {
      return "";
    }
  }

  // 인증코드 입력 후 '인증' 버튼 클릭 시
  const submitIdCode = () => {
    // 백엔드에서 받은 인증코드와 유저가 입력한 인증코드의 일치여부 확인
    if (inputIdCode === identificationCode) {
      setIdentify("fulfilled");
      return;
    }

    setIdentify("rejected"); // 인증 실패
  }

  ////// 렌더링 /////

  // 완료
  // 아이디 찾기에서 성공 시 NotifyUsername 컴포넌트를, 비밀번호 찾기에서 성공 시 SelectAccount 컴포넌트를 렌더링
  if (identify === "fulfilled") {
    return (
      <>
      {successContent}
      </>
    )
  }

  // 아직 인증 중일 경우 렌더링될 컴포넌트들
  return (
      <div>
        <VerifyAccountInput
          label="가입 시 제출한 이메일을 입력해주세요"
          handleInput={onChange}
          defaultValue={input}
          onClick={submitEmail}
          buttonTitle="인증"
          feedbackMsg={getFeedbackMsg()}/>
        {
          identificationCode !== ""
            ?
            <VerifyAccountInput
              label="이메일로 전송된 본인확인 코드 여섯자리를 입력해주세요"
              handleInput={(e) => setInputIdCode(e.target.value)}
              onClick={submitIdCode}
              buttonTitle="확인"
              feedbackMsg={identify === "rejected" ? "본인확인 코드가 일치하지 않아요" : ""}/>
            : <></>
        }
      </div>
  )
}

export default VerifyAccountContent

위 컴포넌트의 흐름은 다음과 같다.

1) 유저가 본인의 이메일을 입력한다.
2) 백엔드는 해당 이메일을 통해 유저 정보를 조회한다.

  • 해당 회원이 존재하면 인증코드를 만들어 해당 이메일로 전송하고, 그 회원의 아이디와 인증코드를 프론트로 리턴한다.
  • 그런 회원정보가 존재하지 않을 시 UsernameNotFoundException을 던진다

3) 유저가 인증코드로 신원 확인을 마치면 아이디 정보 알림 페이지 혹은 비밀번호 변경 페이지로 이동한다.



— 인증 코드 생성 및 메일로 전송하기

인증코드는 apache commons lang 라이브러리의 RandomStringUtils.randomAlphanumeric(int count) 메소드를 사용하였다.
여섯 자리의 영어 대소문자 및 숫자로 설정했다.

메일 전송은 JavaMailSender를 사용하여 간단하게 구현하였다.

우선 필요한 의존성을 추가한다.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.apache.commons:commons-lang3'

인증코드 생성 및 메일 전송

대략적인 코드만 소개한다.

@Service
@Slf4j
@RequiredArgsConstructor
public class GardenerServiceImpl implements GardenerService {
    private final BCryptPasswordEncoder encoder;
    private final GardenerDao gardenerDao;

    private final JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String sendFrom;
    
    /** 생략 **/
    
     @Override
    public Map<String, Object> forgotAccount(String email) {
        List<Gardener> gardeners = gardenerDao.getGardenerByEmail(email);

        if (gardeners.size() == 0) {
            throw new UsernameNotFoundException("해당 이메일로 가입한 회원이 없어요.");
        }

        // 본인확인 코드 만들기
        String identificationCode = RandomStringUtils.randomAlphanumeric(6);

        // 메일 내용 만들기
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("본인 확인 코드는 [ ")
                .append(identificationCode)
                .append(" ] 입니다.");

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(email);
        message.setFrom(sendFrom);
        message.setSubject("[not-a-gardner] 본인확인 코드가 도착했어요.");
        message.setText(stringBuilder.toString());

        mailSender.send(message);

        // 리턴값 만들기
        Map<String, Object> map = new HashMap<>();
        map.put("identificationCode", identificationCode);
        map.put("email", email);

        List<String> gardenerDtos = new ArrayList<>();

        for (Gardener gardener : gardeners) {
            gardenerDtos.add(gardener.getUsername());
        }

        map.put("gardeners", gardenerDtos);

        return map;
    }

우선은 DTO를 따로 만들지 않고 Map에 담아 리턴 해 동작 여부부터 확인했다.

3) 인증 완료 페이지

아이디 알림 컴포넌트인 NotifyUsername.jsx는 단순히 아이디를 알려주고 '로그인 하러가기' 버튼을 통해 로그인 링크로 다시 연결해주는 페이지일 뿐이므로 생략한다.

비밀번호 변경 컴포넌트인 ChangePassword.jsx 또한 비밀번호를 변경하고 로그인 링크를 연결해주는 페이지다.
비밀번호 암호화는 이전과 똑같이 BCryptPasswordEncoder를 사용했다.



3. 미니 기능: 가장 최근 소셜 로그인 서비스 알려주기

티빙 서비스를 이용하던 중, 로그인 창에서 가장 최근 로그인한 소셜 로그인 서비스를 알려주는 기능을 보았다.
유저로서는 아주 편리했고 개발자로선 아주 흥미로운 기능이었다.
다만 어떻게 구현하는지 알 수가 없었을 따름인데, 그러다 정말로 문득 구현 방법이 떠올라 만들었다.
신난다!!

흐름은 다음과 같다.

  1. 소셜 로그인 성공 시 localStorage에 해당 로그인 서비스(provider)를 저장한다.
  2. 로그아웃 시 localStorage에 provider를 제외한 정보만을 지운다.
  3. localStorage의 정보를 통해 로그인 페이지에서 해당 소셜로그인 버튼 아래에 '최근 로그인' 정보를 띄워준다.

1) 로그인

const setUser = async () => {
    localStorage.setItem("login", token);

    const user = await getData("/gardener/gardener-info");

    localStorage.setItem("gardenerId", user.gardenerId);
    localStorage.setItem("name", user.name);
    localStorage.setItem("provider", user.provider); // ⭐️⭐️⭐️⭐️⭐️ 소셜 로그인 정보

    navigate("/", {replace: true});
  }

2) 로그아웃

const logOut = () => {
  const provider = localStorage.getItem("provider");
  localStorage.clear();

  // 소셜로그인을 했던 유저라면
  provider && localStorage.setItem("provider", provider);

  window.location.replace('/login');
}

provider를 임시로 저장해놓은 뒤 clear()로 localStorage를 전체 삭제 후, (소셜로그인 유저라면) 다시 소셜로그인 정보를 입력하는 방식으로 처리했다.

3) 가장 최근 소셜 로그인 서비스 알려주기

const recentLogin = localStorage.getItem("provider");
const providers = ["kakao", "google", "naver"];


const SocialLoginButton = ({provider, recentLogin} ) => {
  const authorizationUrl = `${process.env.REACT_APP_API_URL}/oauth2/authorization`;

  // 애니메이션
  const springProps = useSpring({
    display: 'inline',
    loop: {reverse: true},
    from: {y: 0},
    to: {y: 2},
    config: {
      tension: 300,
      friction: 30,
    }
  })

  return (
    <CCol xs={4} className={"text-center"}>
      <a href={`${authorizationUrl}/${provider}`} className="social-button" id={`${provider}-connect`}></a>
      {
        provider === recentLogin
          ?
          <animated.div style={springProps}>
            <p style={{fontSize: "12px", fontWeight: "bold"}}>최근 로그인</p>
          </animated.div>
          : <></>
      }
    </CCol>
  )
}

localStorage에서 최근 로그인 정보를 가져와 버튼 아래에 '최근 로그인' 정보를 표시한다.
useSpring 훅을 사용해서 약간 동동거리는 애니메이션을 주어 마무리했다.

간단하지만 정말 뿌듯한 기능이다.

0개의 댓글