관리 메뉴

거니의 velog

(42) 리액트 소셜 로그인 5 본문

SpringBoot_React 풀스택 프로젝트

(42) 리액트 소셜 로그인 5

Unlimited00 2024. 3. 8. 12:41

6. 회원정보 수정

* 회원정보 수정은 현재 애플리케이션의 상태로 유지되고 있는 사용자 정보로 화면에 출력하고 수정하게 된다. 이를 위해서 먼저, 화면에서 회원정보를 수정할 수 있도록 처리하는 작업과, API 서버의 작업, API 서버 연동 순으로 처리한다.


(1) 회원정보 수정 화면 처리

* 프로젝트의 components/member 폴더에는 ModifyComponent.js를 추가한다.

import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";

const initState = {
  email: "",
  pw: "",
  nickname: "",
};

const ModifyComponent = () => {
  const [member, setMember] = useState(initState);
  const loginInfo = useSelector((state) => state.loginSlice);

  useEffect(() => {
    setMember({ ...loginInfo, pw: "ABCD" });
  }, [loginInfo]);

  const handleChange = (e) => {
    member[e.target.name] = e.target.value;
    setMember({ ...member });
  };

  return (
    <div className="mt-6">
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">Email</div>
          <input
            className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
            name="email"
            type={"text"}
            value={member.email}
            readOnly
          ></input>
        </div>
      </div>
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">Password</div>
          <input
            className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
            name="pw"
            type={"password"}
            value={member.pw}
            onChange={handleChange}
          ></input>
        </div>
      </div>
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">Nickname</div>
          <input
            className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
            name="nickname"
            type={"text"}
            value={member.nickname}
            onChange={handleChange}
          ></input>
        </div>
      </div>
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap justify-end">
          <button
            type="button"
            className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
          >
            Modify
          </button>
        </div>
      </div>
    </div>
  );
};

export default ModifyComponent;

* pages/member 에는 ModifyPage 를 추가한다.

import React from "react";
import ModifyComponent from "../../components/member/ModifyComponent";
import BasicLayout from "../../layouts/BasicLayout";

const ModifyPage = () => {
  return (
    <BasicLayout>
      <div className=" text-3xl">Member Modify Page</div>
      <div className="bg-white w-full mt-4 p-2">
        <ModifyComponent></ModifyComponent>
      </div>
    </BasicLayout>
  );
};

export default ModifyPage;

* 마지막으로 /member/modify 경로에 대한 라우팅 관련 처리를 추가한다.

import React, { Suspense, lazy } from "react";

const Loading = <div>Loading....</div>;
const Login = lazy(() => import("../pages/member/LoginPage"));
const Logout = lazy(() => import("../pages/member/LogoutPage"));
const KakaoRedirect = lazy(() => import("../pages/member/KakaoRedirectPage"));
const MemberModify = lazy(() => import("../pages/member/ModifyPage"));

const memberRouter = () => {
  return [
    {
      path: "login",
      element: (
        <Suspense fallback={Loading}>
          <Login />
        </Suspense>
      ),
    },
    {
      path: "logout",
      element: (
        <Suspense fallback={Loading}>
          <Logout />
        </Suspense>
      ),
    },
    {
      path: "kakao",
      element: (
        <Suspense fallback={Loading}>
          <KakaoRedirect />
        </Suspense>
      ),
    },
    {
      path: "modify",
      element: (
        <Suspense fallback={Loading}>
          <MemberModify />
        </Suspense>
      ),
    },
  ];
};

export default memberRouter;

* 데이터베이스에 존재하지 않는 사용자이거나 카카오 로그인만 했던 사용자의 경우 자동으로 /member/modify 로 이동하는 것을 확인한다.


(2) API 서버의 회원정보 수정

* API 서버에서는 회원정보를 수정할 수 있도록 기능을 추가한다. 우선 회원정보를 의미하는 MemberDTO는 스프링 시큐리티와 관련해서 생성자 함수가 존재하므로 컨트롤러에서 파라미터 수집 시에 불편하기 때문에 별도로 MemberModifyDTO를 작성한다.

package com.unlimited.mallapi.dto;

import lombok.Data;

@Data
public class MemberModifyDTO {

    private String email;
    private String pw;
    private String nickname;

}

* MemberService와 MemberServiceImpl에 modify() 기능을 추가한다.

package com.unlimited.mallapi.service;

import java.util.stream.Collectors;

import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.domain.Member;
import com.unlimited.mallapi.dto.MemberDTO;
import com.unlimited.mallapi.dto.MemberModifyDTO;

// 트랜잭션 처리 어노테이션, rollbackFor 속성을 사용하여 모든 예외에 대해 롤백 처리
@Transactional(rollbackFor = Exception.class)
public interface MemberService {

    // Kakao의 AccessToken을 이용해 회원 정보를 가져오는 메서드
    MemberDTO getKakaoMember(String accessToken);

    // Entity(Member)를 DTO(MemberDTO)로 변환하는 디폴트 메서드
    default MemberDTO entityToDTO(Member member) {
        // Member 엔티티에서 MemberDTO로 필요한 정보를 복사하여 변환
        MemberDTO dto = new MemberDTO(
                member.getEmail(),
                member.getPw(),
                member.getNickname(),
                member.isSocial(),
                // Member 엔티티에 속한 MemberRole 열거형을 문자열 리스트로 변환
                member.getMemberRoleList().stream().map(memberRole -> memberRole.name()).collect(Collectors.toList()));

        return dto; // 변환된 DTO 반환
    }

    /**
     * 회원 정보를 수정합니다.
     *
     * @param memberModifyDTO 수정할 회원 정보를 담고 있는 DTO 객체
     */
    void modifyMember(MemberModifyDTO memberModifyDTO);

}

* MemberServiceImpl에서는 변경이 가능한 패스워드와 닉네임을 수정하고 social 속성값을 false 로 변경한다.

    /**
     * 회원 정보를 수정합니다.
     *
     * @param memberModifyDTO 수정할 회원 정보를 담고 있는 DTO 객체
     */
    @Override
    public void modifyMember(MemberModifyDTO memberModifyDTO) {
        // 회원 이메일을 기반으로 회원을 조회
        Optional<Member> result = memberRepository.findById(memberModifyDTO.getEmail());

        // 회원이 존재하지 않으면 예외를 던집니다.
        Member member = result.orElseThrow();

        // 비밀번호를 암호화하여 변경합니다.
        member.changePw(passwordEncoder.encode(memberModifyDTO.getPw()));

        // 소셜 로그인 여부를 변경합니다.
        member.changeSocial(false);

        // 닉네임을 변경합니다.
        member.changeNickName(memberModifyDTO.getNickname());

        // 변경된 회원 정보를 저장합니다.
        memberRepository.save(member);
    }

* SocialController에 회원정보 수정 시에 사용할 메서드를 추가한다.

    /**
     * 회원 정보를 수정하는 엔드포인트입니다.
     *
     * @param memberModifyDTO 수정할 회원 정보를 담은 DTO 객체
     * @return 수정 결과를 나타내는 Map (예: {"result": "modified"})
     */
    @PutMapping("/api/member/modify")
    public Map<String, String> modify(@RequestBody MemberModifyDTO memberModifyDTO) {
        log.info("회원 수정: " + memberModifyDTO);

        // 회원 정보 수정 서비스를 호출하여 회원 정보를 변경합니다.
        memberService.modifyMember(memberModifyDTO);

        // 수정 결과를 포함한 Map을 반환합니다.
        return Map.of("result", "modified");
    }

(3) 리액트와 API 연동

* 리액트에서는 작성된 /api/member/modify 경로를 호출하는 코드를 memberApi.js에 추가한다. 회원정보 수정은 로그인이 될 수 있는 사용자만 가능하므로 jwtAxios를 이용한다.

import axios from "axios";
import { API_SERVER_HOST } from "./todoApi";
import jwtAxios from "../util/jwtUtil";

const host = `${API_SERVER_HOST}/api/member`;

export const loginPost = async (loginParam) => {
  const header = { headers: { "Content-Type": "x-www-form-urlencoded" } };

  const form = new FormData();
  form.append("username", loginParam.email);
  form.append("password", loginParam.pw);

  const res = await axios.post(`${host}/login`, form, header);

  return res.data;
};

export const modifyMember = async (member) => {
  const res = await jwtAxios.put(`${host}/modify`, member);
  return res.data;
};

* 화면을 구성하는 components/member/ModifyComponent에서는 modifyMember()를 호출하도록 수정한다.

import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { modifyMember } from "../../api/memberApi";

const initState = {
  email: "",
  pw: "",
  nickname: "",
};

const ModifyComponent = () => {
  (...)

  const handleClickModify = () => {
    modifyMember(member);
  };

  return (
    <div className="mt-6">
      (...)
      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap justify-end">
          <button
            type="button"
            className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
            onClick={handleClickModify}
          >
            Modify
          </button>
        </div>
      </div>
    </div>
  );
};

export default ModifyComponent;

* 브라우저에서 카카오 로그인 후 에 회원정보를 수정해 보자. 화면에는 변화가 없지만 데이터베이스에는 변경된 내용을 확인할 수 있는데 특히 기존에 소셜 회원(social) 속성값이 변경된 것을 볼 수 있다.


[수정 후 다시 로그인 하기]

* API 서버에서 정상적으로 회원정보가 수정되었다면 결과를 보여주고 다시 로그인 화면으로 이동해서 로그인 해보도록 한다. 일반 회원으로 전환된 사용자는 카카오 로그인으로 하거나 일반 로그인 모두 가능하다.

import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { modifyMember } from "../../api/memberApi";
import useCustomLogin from "../../hooks/useCustomLogin";
import ResultModal from "../common/ResultModal";

const initState = {
  email: "",
  pw: "",
  nickname: "",
};

const ModifyComponent = () => {
  const [member, setMember] = useState(initState);
  const loginInfo = useSelector((state) => state.loginSlice);
  const { moveToLogin } = useCustomLogin();
  const [result, setResult] = useState();

  useEffect(() => {
    setMember({ ...loginInfo, pw: "ABCD" });
  }, [loginInfo]);

  const handleChange = (e) => {
    member[e.target.name] = e.target.value;
    setMember({ ...member });
  };

  const handleClickModify = () => {
    modifyMember(member).then((result) => {
      setResult("Modified");
    });
  };

  const closeModal = () => {
    setResult(null);
    moveToLogin();
  };

  return (
    <div className="mt-6">
      {result ? (
        <ResultModal
          title={"회원정보"}
          content={"정보수정완료"}
          callbackFn={closeModal}
        ></ResultModal>
      ) : (
        <></>
      )}

      (...)
    </div>
  );
};

export default ModifyComponent;

* 회원정보가 수정되면 로그아웃이 되면서 로그인 페이지로 이동하게 된다. 이미 일반 회원으로 전환되었기 때문에 카카오 로그인을 하더라도 / 경로로 이동하게 된다.