회원관리(MariaDB) - Part3 회원 정보 수정

Project Diary/React + MariaDB (PicShare WebApp)

사용자가 회원 정보를 수정할 수 있는 기능을 구현합니다. 사용자는 닉네임, 비밀번호, 프로필 사진을 변경할 수 있으며, 이를 위해 프론트엔드와 백엔드가 상호작용합니다. 데이터베이스에 저장된 정보를 업데이트하고, 중복된 닉네임을 체크하며, 새로운 프로필 사진을 업로드하는 과정을 포함합니다.


 

프론트엔드

 

1. 회원 정보 수정 페이지 (ProfileModify.jsx)

 

1-1 초기 상태 및 사용자 정보 로드

import React, { useRef, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import styled from "styled-components";
import axios from "axios";
import { fetchUsers, userLogout } from "@/store/member";

const serverUrl = import.meta.env.VITE_API_URL;

const ProfileModifyBlock = styled.div`
  // 생략: 스타일 관련 코드
`;

const ProfileModify = () => {
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const userNicknameRef = useRef();
  const [userInfo, setUserInfo] = useState({
    userNickname: "",
    currentPassword: "",
    newPassword: "",
    photo: "",
  });
  const [error, setError] = useState({});
  const [success, setSuccess] = useState({});
  const [useDefaultProfile, setUseDefaultProfile] = useState(false);
  const [profilePreview, setProfilePreview] = useState("");

  const user = useSelector((state) => state.members.user);

  useEffect(() => {
    if (user) {
      setProfilePreview(`${serverUrl}/uploads/${user.profilePicture}`);
      setUserInfo((prevState) => ({
        ...prevState,
        userNickname: user.userNickname,
      }));
    }
  }, [user]);

  // 이하 생략
};
  • useState 및 useEffect: 사용자 정보를 로드하고 초기 상태를 설정합니다.
  • useSelector: Redux 스토어에서 현재 사용자 정보를 가져옵니다.
  • useEffect: 사용자 정보가 변경될 때마다 프로필 프리뷰와 닉네임을 설정합니다.

1-2 프로필 이미지 변경

const handleCheckboxChange = () => {
  setUseDefaultProfile((prevUseDefaultProfile) => {
    const newUseDefaultProfile = !prevUseDefaultProfile;
    setProfilePreview(
      newUseDefaultProfile
        ? `${serverUrl}/uploads/defaultProfile.jpg`
        : `${serverUrl}/uploads/${user.profilePicture}`
    );
    return newUseDefaultProfile;
  });
};

const handleFileChange = (e) => {
  const file = e.target.files[0];
  setUserInfo((prevUserInfo) => ({ ...prevUserInfo, photo: file }));
  setProfilePreview(URL.createObjectURL(file));
};
  • handleCheckboxChange: 체크박스를 통해 기본 프로필 이미지 사용 여부를 토글합니다.
    - 기본 프로필 사용 시: defaultProfile.jpg를 프리뷰로 설정합니다.
    -  사용자 정의 프로필 사용 시: 업로드된 프로필 사진을 프리뷰로 설정합니다.
  • handleFileChange: 파일 입력 시 선택한 파일을 userInfo 상태에 저장하고, 프로필 사진 미리보기를 업데이트합니다.
    -  파일 선택 시: URL.createObjectURL(file)을 사용하여 미리보기 이미지를 설정합니다.

1-3 닉네임 중복 체크 및 입력 처리

const handleChange = async (e) => {
  const { value, name } = e.target;
  setUserInfo((userInfo) => ({ ...userInfo, [name]: value }));
  setError((error) => ({ ...error, [name]: "" }));
  setSuccess((success) => ({ ...success, [name]: "" }));

  // 닉네임 중복 체크
  if (name === "userNickname" && value) {
    try {
      await axios.post(`${serverUrl}/auth/check-nickname`, {
        userNickname: value,
      });
      setSuccess((success) => ({
        ...success,
        userNickname: "사용 가능한 닉네임입니다.",
      }));
    } catch (err) {
      if (err.response && err.response.data) {
        setError((error) => ({
          ...error,
          userNickname: err.response.data.message,
        }));
      }
    }
  }
};
  • handleChange: 입력 필드가 변경될 때 호출
    -  사용자 정보를 업데이트하고, 오류 메시지를 초기화합니다.
    -  닉네임 중복 체크를 수행합니다.

1-4 프로필 업데이트 처리

const updateprofile = async (e) => {
  e.preventDefault();
  const formData = new FormData();
  formData.append("userNo", user.userNo);
  formData.append("userNickname", userInfo.userNickname);
  formData.append("currentPassword", userInfo.currentPassword);
  formData.append("newPassword", userInfo.newPassword);
  if (!useDefaultProfile && userInfo.photo) {
    formData.append("photo", userInfo.photo);
  }

  try {
    const res = await axios.put(
      `${serverUrl}/auth/update-profile`,
      formData,
      {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      }
    );
    if (res.data.affectedRows >= 1) {
      alert("회원정보가 수정되었습니다. 변경 사항을 적용하려면 로그아웃 후 다시 로그인해 주세요");
      dispatch(userLogout(user.userNo));
      navigate("/login");
    } else {
      alert("회원수정이 실패했습니다.");
    }
  } catch (err) {
    console.error("서버 오류:", err);
    if (err.response && err.response.data) {
      const { field, message } = err.response.data;
      setError((prevError) => ({ ...prevError, [field]: message }));
    } else {
      console.error(err);
    }
  }
};
  • updateprofile: 사용자 프로필 정보를 업데이트하는 함수
    - FormData를 사용하여 파일 업로드와 함께 데이터를 전송합니다.
    - 서버로부터 응답을 받아 성공 여부에 따라 알림을 표시합니다.
    - 업데이트된 정보를 적용하려면 로그아웃 후 다시 로그인하도록 안내합니다.

1-5 회원 탈퇴 처리

const handleDelete = (userNo) => {
  console.log("탈퇴회원", userNo);
  const confirmDelete = window.confirm(
    "정말 회원 탈퇴를 하시겠습니까?\n\n" +
      "회원 탈퇴 시, 귀하의 계정 및 모든 데이터가 영구적으로 삭제됩니다.\n"
  );

  if (confirmDelete) {
    axios
      .delete(`${serverUrl}/auth/delete`, { params: { userNo } })
      .then((res) => {
        if (res) {
          console.log(res.data);
          dispatch(fetchUsers());
          alert("회원탈퇴가 완료되었습니다.");
          dispatch(userLogout());
          navigate("/login");
        } else {
          alert("삭제하지 못했습니다.");
        }
      })
      .catch((err) => console.log(err));
  }
};
  • handleDelete: 회원 탈퇴를 처리하는 함수
    - 사용자에게 확인 메시지를 표시하고, 동의하면 탈퇴 요청을 서버로 전송합니다.
    - 성공적으로 탈퇴되면 알림을 표시하고, 사용자 로그아웃 및 로그인 페이지로 이동합니다.

 

백엔드

 

1. 회원 정보 수정 API 엔드포인트 (authRouter.js)

 

2-1 회원 정보 수정 엔드포인트

authRouter.put("/update-profile", upload.single("photo"), (req, res) => {
  const { userNo, userNickname, currentPassword, newPassword } = req.body;
  const photo = req.file ? req.file.filename : "defaultProfile.jpg";

  // 닉네임과 프로필 사진 업데이트
  if (photo || userNickname) {
    let updateQuery = "UPDATE users SET";
    let queryParams = [];

    if (photo) {
      updateQuery += " profilePicture = ?";
      queryParams.push(photo);
    }

    if (userNickname) {
      if (queryParams.length > 0) updateQuery += ",";
      updateQuery += " userNickname = ?";
      queryParams.push(userNickname);
    }

    updateQuery += " WHERE userNo = ?";
    queryParams.push(userNo);

    db.query(updateQuery, queryParams, (err, result) => {
      if (err) {
        return res.status(500).json({
          message: "서버 오류가 발생했습니다. 다시 시도해주세요.",
        });
      }
      if (newPassword) {
        updatePassword();
      } else {
        res.status(200).json({ affectedRows: result.affectedRows });
      }
    });
  } else if (newPassword) {
    updatePassword();
  }

  function updatePassword() {
    // 현재 비밀번호 확인
    db.query(
      "SELECT * FROM users WHERE userNo = ?",
      [userNo],
      (err, results) => {
        if (err) {
          return res.status(500).json({
            message: "서버 오류가 발생했습니다. 다시 시도해주세요.",
          });
        }
        const user = results[0];
        if (user.password !== currentPassword) {
          return res.status(400).json({
            field: "currentPassword",
            message: "현재 비밀번호가 일치하지 않습니다.",
          });
        }

        // 새로운 비밀번호로 업데이트
        db.query(
          "UPDATE users SET password = ? WHERE userNo = ?",
          [newPassword, userNo],
          (err, results) => {
            if (err) {
              return res.status(500).json({
                message: "서버 오류가 발생했습니다. 다시 시도해주세요.",
              });
            }
            res.status(200).json({ affectedRows: results.affectedRows });
          }
        );
      }
    );
  }
});

 

sql 쿼리문
authRouter.put("/update-profile", upload.single("photo"), (req, res) => { ... });
  • 회원 정보 수정 요청을 처리합니다.
  • upload.single("photo")
    Multer를 사용하여 단일 파일 업로드를 처리합니다.
  • 닉네임과 프로필 사진을 업데이트합니다.
  • 비밀번호가 변경되면 현재 비밀번호를 확인하고 새로운 비밀번호로 업데이트합니다.

프로필이미지, 닉네임 변경, 비밀번호 변경