회원관리 - 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); :
업데이트된 프로필 정보를 응답으로 반환합니다.