회원관리 - Part4. 회원정보 수정

Project Diary/Next.js + Prisma + MariaDB (KiloFlow)

회원정보 수정 기능은 개인정보와 프로필 정보를 업데이트할 수 있도록 합니다.


 

프론트엔드

 

1. 사용자 정보 수정 페이지 컴포넌트

 

1-1 상태값 정의

const [activeTab, setActiveTab] = useState("personalInfo"); // 현재 활성화된 탭을 나타내는 상태값
const [currentUserInfo, setCurrentUserInfo] = useState<any>(null); // 현재 사용자의 기본 정보를 저장하는 상태값
const [currentUserProfile, setCurrentUserProfile] = useState<any>(null); // 현재 사용자의 프로필 정보를 저장하는 상태값
const router = useRouter(); // Next.js의 useRouter 훅으로, 페이지 이동을 처리

 

1-2 유저 정보 불러오기

useEffect(() => {
  // 유저 정보 불러오기 함수
  const fetchCurrentUser = async () => {
    const token = localStorage.getItem("token"); // 로컬 저장소에서 토큰을 가져옴
    if (token) {
      const res = await fetch("/api/auth/me", {
        headers: {
          Authorization: `Bearer ${token}`, // API 호출 시 토큰을 헤더에 포함
        },
      });

      if (res.ok) {
        const data = await res.json();
        setCurrentUserInfo(data.user); // 사용자 정보를 상태값에 저장
        setCurrentUserProfile(data.userProfile); // 사용자 프로필 정보를 상태값에 저장
      } else {
        localStorage.removeItem("token");
        router.push("/auth/login"); // 토큰이 유효하지 않으면 로그인 페이지로 이동
      }
    } else {
      router.push("/auth/login"); // 토큰이 없으면 로그인 페이지로 이동
    }
  };

  fetchCurrentUser();
}, [router]);

토큰을 이용해 현재 사용자의 정보를 API로부터 불러옵니다.

  • localStorage.getItem("token") :
    로컬 저장소에서 토큰을 가져옵니다.
  • fetch("/api/auth/me", { headers: { Authorization: \Bearer ${token}` } })` :
    API 호출 시 토큰을 헤더에 포함하여 보냅니다.
  • localStorage.removeItem("token"), router.push("/auth/login") :
    토큰이 없거나 유효하지 않으면 로그인 페이지로 리다이렉트합니다.

1-3 콘텐츠 렌더링

const renderContent = () => {
  switch (activeTab) {
    case "personalInfo":
      return <PersonalInfo currentUserInfo={currentUserInfo} />; // 개인정보 수정 컴포넌트 렌더링
    case "profileModify":
      return <ProfileModify currentUserProfile={currentUserProfile} />; // 프로필 수정 컴포넌트 렌더링
    case "participatingChat":
      return <ParticipatingChat currentUserInfo={currentUserInfo} />; // 참여 중인 채팅 컴포넌트 렌더링
    default:
      return null;
  }
};
  • activeTab의 값이 "personalInfo"이면 PersonalInfo컴포넌트(개인정보 수정)컴포넌트를 렌더링 합니다
  • activeTab의 값이 "profileModify"이면 ProfileModify컴포넌트(프로필 수정)컴포넌트를 렌더링 합니다
  • activeTab의 값이 "participatingChat"이면 ParticipatingChat컴포넌트(참여중인 채팅방)컴포넌트를 렌더링 합니다

 

2.  개인정보 수정 컴포넌트

 

2-1 유저 정보 로드

useEffect(() => {
  if (currentUserInfo) {
    setNickname(currentUserInfo.nickname); // 닉네임 설정
    setProfilePreview(currentUserInfo.profile_image); // 프로필 이미지 미리보기 설정
    setProfileImage(currentUserInfo.profile_image); // 프로필 이미지 설정
  }
}, [currentUserInfo]);

 

2-2 프로필 이미지 변경 핸들러

const handleProfileImageClick = () => {
  setShowButtons((prevState) => !prevState); // 이미지 변경 버튼을 토글
};

 

2-3 이미지 선택 핸들러

const handleImageSelect = (src: string) => {
  setProfilePreview(src); // 선택한 이미지 미리보기 설정
  setProfileImage(src); // 선택한 이미지 설정
  setShowButtons(false); // 이미지 변경 버튼 숨김
};

 

2-4 파일 변경 핸들러

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.files && e.target.files[0]) {
    const file = e.target.files[0]; // 업로드된 파일 가져오기
    setProfileImage(file); // 파일을 상태값으로 설정
    const fileUrl = URL.createObjectURL(file); // 파일의 URL 생성
    setProfilePreview(fileUrl); // 파일 미리보기 URL 설정
  }
};

 

2-5 폼 제출 핸들러

const handleSubmit = async (event: React.FormEvent) => {
  event.preventDefault(); // 기본 폼 제출 동작을 방지
  setError("");

  const formData = new FormData();
  formData.append("email", currentUserInfo.email); // 이메일 추가
  formData.append("nickname", nickname); // 닉네임 추가
  if (typeof profileImage === "string") {
    formData.append("profile_image_url", profileImage); // 프로필 이미지 URL 추가
  } else {
    formData.append("profile_image", profileImage); // 프로필 이미지 파일 추가
  }
  if (currentPassword) formData.append("currentPassword", currentPassword); // 현재 비밀번호 추가
  if (newPassword) formData.append("newPassword", newPassword); // 새 비밀번호 추가

  try {
    const res = await fetch("/api/modify/personal-info-modify", {
      method: "POST",
      body: formData,
    });

    if (res.ok) {
      const data = await res.json();
      setError("");
      window.alert("개인정보 수정이 완료되었습니다."); // 성공 알림
      router.reload(); // 페이지 새로고침으로 변경사항 반영
    } else {
      const data = await res.json();
      setError(data.message); // 오류 메시지 설정
    }
  } catch (err) {
    setError("An unexpected error occurred"); // 예기치 않은 오류 메시지 설정
  }
};
  •  fetch("/api/modify/personal-info-modify", { method: "POST", body: formData }) :
    서버로 폼 데이터를 전송합니다.

 

3. 프로필 정보 수정 컴포넌트

 

3-1 상태값 정의

const [height, setHeight] = useState<number>(currentUserProfile.height); // 키
const [weight, setWeight] = useState<number>(currentUserProfile.weight); // 몸무게
const [targetWeight, setTargetWeight] = useState<number>(currentUserProfile.target_weight); // 목표 몸무게
const [difficulty, setDifficulty] = useState<string>(currentUserProfile.difficulty); // 감량 난이도
const [dailyCalories, setDailyCalories] = useState<number | null>(null); // 일일 권장 섭취 칼로리
const [totalDays, setTotalDays] = useState<number | null>(null); // 총 감량 일수
const router = useRouter(); // Next.js의 useRouter 훅으로, 페이지 이동을 처리

 

3-2 프로필 정보 로드

useEffect(() => {
  if (currentUserProfile) {
    const bmr = calculateBMR(currentUserProfile.weight, currentUserProfile.height); // 기초 대사량 계산
    const { dailyCalories, totalDays } = calculateDailyCalories(
      currentUserProfile.weight,
      currentUserProfile.target_weight,
      currentUserProfile.difficulty,
      bmr
    ); // 일일 권장 섭취 칼로리와 총 감량 기간 계산
    setDailyCalories(dailyCalories); // 일일 권장 섭취 칼로리 설정
    setTotalDays(totalDays); // 총 감량 기간 설정
  }
}, [currentUserProfile]);

currentUserProfile가 변경될 때마다 프로필 정보를 상태에 설정합니다.

  • setDailyCalories, setTotalDays : 계산된 값을 상태값으로 설정합니다.

3-3 프로필 정보 수정 API 호출

const handleSubmit = async (event: React.FormEvent) => {
  event.preventDefault(); // 기본 폼 제출 동작을 방지
  try {
    const token = localStorage.getItem("token"); // 로컬 저장소에서 토큰 가져오기
    const res = await fetch("/api/modify/profile-modify", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`, // 토큰을 헤더에 포함
      },
      body: JSON.stringify({
        user_id: currentUserProfile.user_id, // 사용자 ID
        height, // 키
        weight, // 몸무게
        targetWeight, // 목표 몸무게
        difficulty, // 감량 난이도
        dailyCalories, // 일일 권장 섭취 칼로리
        totalDays, // 총 감량 일수
      }),
    });

    if (res.ok) {
      const data = await res.json();
      window.alert("목표 수정이 완료되었습니다."); // 성공 알림
      router.push("/personal?activeTab=profileModify"); // 프로필 수정 페이지로 이동
    } else {
      const data = await res.json();
      console.error(data.message); // 오류 메시지 출력
    }
  } catch (error) {
    console.error("프로필 업데이트 중 오류가 발생했습니다.", error); // 예기치 않은 오류 메시지 출력
  }
};
  • fetch("/api/modify/profile-modify", { method: "POST", headers: { "Content-Type": "application/json", Authorization: Bearer ${token} }, body: JSON.stringify({ ... }) }) :
    서버로 폼 데이터를 전송합니다.

 

백엔드

 

1. 개인정보 수정 API

 

1-1 Multer 미들웨어 처리

const runMiddleware = (
  req: NextApiRequest,
  res: NextApiResponse,
  fn: Function
) => {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result); // 오류 발생 시 프로미스 거부
      }
      return resolve(result); // 성공 시 프로미스 해결
    });
  });
};

 

1-2 개인정보 수정 API 핸들러

// Multer 미들웨어 실행
await runMiddleware(req, res, upload.single("profile_image"));

const { email, nickname, currentPassword, newPassword, profile_image_url } = req.body;

if (!email || !nickname) {
  return res
    .status(400)
    .json({ message: "Email and nickname are required" }); // 이메일과 닉네임이 없으면 400 응답
}

// 유저 정보 가져오기
const user = await prisma.users.findUnique({
  where: {
    email: email,
  },
});

if (!user) {
  return res.status(404).json({ message: "User not found" }); // 사용자가 없으면 404 응답
}

// 비밀번호 변경 로직
if (currentPassword && newPassword) {
  if (currentPassword !== user.password) {
    return res
      .status(400)
      .json({ message: "현재 비밀번호가 일치하지 않습니다." }); // 현재 비밀번호가 일치하지 않으면 400 응답
  }
  user.password = newPassword; // 비밀번호 업데이트
}

// 프로필 이미지 경로 설정
const profileImageUrl = req.file
  ? `/uploads/${req.file.filename}` // 업로드된 파일의 경로 설정
  : profile_image_url || user.profile_image; // 기존 프로필 이미지 경로 설정

// 유저 정보 업데이트
const updatedUser = await prisma.users.update({
  where: { email: user.email },
  data: {
    nickname,
    profile_image: profileImageUrl,
    password: user.password,
  },
});
  • const user = await prisma.users.findUnique({ where: { email } }); :
    데이터베이스에서 사용자 정보를 가져옵니다.
  • if (currentPassword && newPassword) { ... } :
    비밀번호 변경 로직을 처리합니다.
  • const profileImageUrl = req.file ? ... :
    프로필 이미지 경로를 설정합니다.
  • const updatedUser = await prisma.users.update({ ... }); :
    사용자 정보를 업데이트합니다.

 

2. 프로필 정보 수정 API

import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../../lib/prisma";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: `Method ${req.method} Not Allowed` }); // POST 메소드가 아니면 405 응답
  }

  const {
    user_id,
    height,
    weight,
    targetWeight,
    difficulty,
    dailyCalories,
    totalDays,
  } = req.body; // 요청 본문에서 필요한 데이터 추출

  try {
    const updatedProfile = await prisma.userProfile.update({
      where: { user_id: user_id }, // 사용자 ID로 프로필 정보 찾기
      data: {
        height, // 키 업데이트
        weight, // 몸무게 업데이트
        target_weight: targetWeight, // 목표 몸무게 업데이트
        difficulty, // 감량 난이도 업데이트
        daily_calories: dailyCalories, // 일일 권장 섭취 칼로리 업데이트
        updated_at: new Date(), // 업데이트 시간 설정
      },
    });

    return res.status(200).json(updatedProfile); // 성공적으로 업데이트된 프로필 정보 반환
  } catch (error) {
    console.error("프로필 업데이트 중 오류가 발생했습니다.", error); // 오류 메시지 출력
    return res
      .status(500)
      .json({ message: "프로필 업데이트 중 오류가 발생했습니다." }); // 500 응답
  }
}
  • if (req.method !== "POST") { ... } :
    요청 메소드가 POST인지 확인합니다.
  • const updatedProfile = await prisma.userProfile.update({ ... }); :
    데이터베이스에서 프로필 정보를 업데이트합니다.
  • return res.status(200).json(updatedProfile); :
    업데이트된 프로필 정보를 응답으로 반환합니다.

회원정보 수정
프로필정보 수정