회원관리 - Part3 프로필 정보 초기설정 with 챗봇

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

사용자는 로그인 후 키, 몸무게, 목표 몸무게, 감량 난이도를 설정하여 맞춤형 권장 섭취 칼로리를 계산받을 수 있습니다. 초기 설정이 완료된 후에는 더 이상 초기 설정 페이지가 뜨지 않으며, 회원가입 후 초기 설정은 한 번만 할 수 있습니다.


데이터베이스 스키마

 

1. users 테이블

model users {
  user_id                Int              @id @default(autoincrement())
  email                  String           @unique
  password               String
  nickname               String
  profile_image          String           @default("default_image_url")
  isInitialSetupComplete Boolean          @default(false)
  created_at             DateTime         @default(now())
  userProfile            UserProfile?     // 추가된 부분

  @@index([user_id])
}
관계 설정
  userProfile            UserProfile?     // 추가된 부분
  • UserProfile 모델과의 관계를 정의합니다.
  • 이 필드는 각 사용자가 하나의 프로필 정보를 가지도록 설정하는 데 필요합니다.

2. UserProfile 모델

model UserProfile {
  id            Int      @id @default(autoincrement())
  user_id       Int      @unique
  height        Float
  weight        Float
  target_weight Float
  difficulty    String
  daily_calories Int
  created_at    DateTime @default(now())
  updated_at    DateTime @updatedAt

  user          users    @relation(fields: [user_id], references: [user_id])

  @@index([user_id])
}
관계 설정
user          users    @relation(fields: [user_id], references: [user_id])
  • users 모델과의 관계를 정의합니다.
  • user_id를 참조하여 사용자와 프로필 간의 관계를 설정합니다.

 

왜 관계 설정 필드가 필요한가?

  1. 관계 설정
    - users모델의 userProfile 필드는 UserProfile 모델과의 일대일 관계를 설정하여, 각 사용자가 고유한 프로필 정보를 가지도록 보장합니다. 이는 users 테이블과 UserProfile 테이블 간의 참조 관계를 명확하게 정의합니다.
    - UserProfile 모델의 user 필드는 users 모델과의 관계를 설정하여, user_id를 참조합니다. 이를 통해 사용자가 프로필 정보를 가지며, 두 테이블 간의 관계가 명확해집니다.
  2. 데이터 접근의 용이성
    - users모델의 userProfile 필드를 통해 사용자의 프로필 정보를 쉽게 접근할 수 있습니다. 예를 들어, 특정 사용자의 프로필 정보를 가져오려면 users 테이블의 userProfile 필드를 통해 바로 접근할 수 있습니다.
    - UserProfile 모델의 user 필드를 통해 특정 프로필의 사용자 정보를 쉽게 접근할 수 있습니다.
  3. 데이터 무결성
    - userProfile 필드는 각 사용자마다 유일한 프로필을 가지도록 보장하여 데이터 무결성을 유지합니다.
    - user 필드는 프로필 정보가 정확한 사용자와 연결되도록 보장합니다.

 


 

프론트엔드

초기 설정 페이지 (initialSetting.tsx)

 

1. 사용자 입력 상태 관리

const [step, setStep] = useState(0);
const [height, setHeight] = useState(160);
const [weight, setWeight] = useState(60);
const [targetWeight, setTargetWeight] = useState(60);
const [difficulty, setDifficulty] = useState("");
const [dailyCalories, setDailyCalories] = useState<number | null>(null);
const [totalDays, setTotalDays] = useState<number | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const chatContainerRef = useRef<HTMLDivElement>(null);

 

2. 챗봇 메시지 처리

useEffect(() => {
  if (step < questions.length && step <= 4) {
    // 타이머를 설정하여 일정 시간 후 챗봇 메시지를 자동으로 전송
    const timer = setTimeout(() => {
      setMessages((prevMessages) => [
        // 기존 메시지 목록에 새로운 챗봇 메시지를 추가
        ...prevMessages,
        { type: "bot", text: questions[step].text },
      ]);
      // 단계(step)를 하나 증가시켜 다음 메시지로 이동
      setStep((prevStep) => prevStep + 1);
    }, questions[step].delay); // 각 질문마다 다른 지연 시간 설정

    // 타이머를 정리하여 메모리 누수를 방지
    return () => clearTimeout(timer);
  } else if (step < questions.length) {
    // 초기 단계가 지나면 타이머 없이 바로 챗봇 메시지 전송
    setMessages((prevMessages) => [
      ...prevMessages,
      { type: "bot", text: questions[step].text },
    ]);
  }
}, [step]);

 

위 코드를 순서대로 자세히 설명하겠습니다.

 

2-1

useEffect(() => {
 // 챗봇 메시지 처리
}, [step]);

컴포넌트가 마운트되거나 step 상태가 변경될 때마다 실행됩니다.

 

2-2

if (step < questions.length && step <= 4)

현재 step이 questions 배열의 길이보다 작고, 4 이하인 경우에만 실행됩니다. 즉, 초기  4단계에만 해당 로직이 적용됩니다. 
초기 4단계에는 사용자의 응답을 받을 필요가 없기 때문입니다. 

(4단계 이전의 메시지들이 시간지연을 갖고 전송되게)

 

2-3

const timer = setTimeout(() => { ... }, questions[step].delay);

일정 시간(questions[step].delay) 후에 setTimeout 내부의 함수가 실행됩니다.

 

2-4

setMessages((prevMessages) => [ ...prevMessages, { type: "bot", text: questions[step].text }, ]);

이전 메시지 목록에 새로운 챗봇 메시지 { type: "bot", text: questions[step].text }을 추가합니다.

 

2-5

setStep((prevStep) => prevStep + 1);

현재 단계(step)를 하나 증가시킵니다.

 

2-6

else if (step < questions.length) { ... }

step이 questions 배열의 길이보다 작지만, 4보다 큰 경우에 실행됩니다.

 

2-7

setMessages((prevMessages) => [ ...prevMessages, { type: "bot", text: questions[step].text }, ]);

이전 메시지 목록에 새로운 메시지를 추가합니다.

 

 

단계에 따라 챗봇 메시지를 처리하는 이유

  • 1,2,3,4 단계는 프로그램에 대해 설명해주는 메세지로 사용자의 답변이 필요하지 않습니다.
  • 4딘계 이하는 사용자의 답변을 받지않아도 자동으로 전송되게 하거 5단계 부터는 사용자의 답변이 입력되면 바로 전송되도록 하기 위함입니다..

 

3. 사용자 입력 처리 및 다음 단계로 이동(5단계 이후)

const handleNextStep = (userInput: string) => {
  // 사용자가 입력한 값을 메시지 목록에 추가
  setMessages((prevMessages) => [
    ...prevMessages,
    { type: "user", text: userInput },
  ]);

  // 각 단계에 따라 사용자가 입력한 값을 상태에 저장
  if (step === 5) setHeight(parseInt(userInput)); // 키 설정
  if (step === 6) setWeight(parseInt(userInput)); // 몸무게 설정
  if (step === 7) setTargetWeight(parseInt(userInput)); // 목표 몸무게 설정

  if (step === 8) {
    // 감량 난이도 설정 및 권장 섭취 칼로리 계산
    setDifficulty(userInput);
    const bmr = calculateBMR(weight, height); // 기초 대사량 계산
    const { dailyCalories, totalDays } = calculateDailyCalories(
      weight,
      targetWeight,
      userInput,
      bmr
    ); // 일일 권장 섭취 칼로리와 총 감량 기간 계산
    
    setDailyCalories(dailyCalories); // 일일 권장 섭취 칼로리 설정
    setTotalDays(totalDays); // 총 감량 기간 설정

    // 결과 메시지를 사용자에게 보여줌
    setMessages((prevMessages) => [
      ...prevMessages,
      { type: "bot", text: `너의 하루 권장 섭취 칼로리는 ${dailyCalories} kcal이야!` },
      { type: "bot", text: `총 감량 기간은 ${totalDays}일이야!` },
      { type: "bot", text: "그럼 초기 설정을 끝낼까?" },
    ]);
  }

  // 단계를 하나 증가시킴
  setStep((prevStep) => prevStep + 1);
  setInputValue(""); // 입력 필드를 초기화
};

(설명 : 주석 참고)

 

4. 초기 설정 완료 요청

const completeInitialSetting = async () => {
  setLoading(true);
  setError("");
  try {
    const token = localStorage.getItem("token");
    const res = await fetch("/api/auth/complete-initial-setting", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        height,
        weight,
        targetWeight,
        difficulty,
        dailyCalories,
        totalDays,
      }),
    });
    if (res.ok) {
      router.push("/");
    } else {
      setError("초기 설정 완료에 실패했습니다.");
    }
  } catch (error) {
    setError("초기 설정 완료 중 오류가 발생했습니다.");
    console.error("초기 설정 완료 중 오류가 발생했습니다.", error);
  } finally {
    setLoading(false);
  }
};

사용자가 입력한 초기 설정 정보를 서버에 전송하여 저장을 요청합니다.

 


 

백엔드

초기 설정 API 엔드포인트 (complete-initial-setting.ts)

 

1. JWT 토큰 검증 및 데이터 파싱

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

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
  }

  const token = req.headers.authorization?.split(" ")[1];
  if (!token) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  try {
    const decoded: any = jwt.verify(token, process.env.JWT_SECRET || "secret");
    const { height, weight, targetWeight, difficulty, dailyCalories } = req.body;

    // jwt.verify : JWT 토큰을 검증하고, 유저 정보를 디코드합니다.
    // req.body : 클라이언트로부터 받은 초기 설정 정보

요청 메소드가 POST인지 확인하고, JWT 토큰을 검증하며, 클라이언트로부터 받은 초기 설정 정보를 파싱합니다.

 

2. 프로필 정보 저장 및 초기 설정 완료 표시

    await prisma.userProfile.create({
      data: {
        user_id: decoded.userId,
        height,
        weight,
        target_weight: targetWeight,
        difficulty,
        daily_calories: dailyCalories,
      },
    });

    // prisma.userProfile.create : 프로필 정보를 데이터베이스에 저장

    await prisma.users.update({
      where: { user_id: decoded.userId },
      data: { isInitialSetupComplete: true },
    });

    // prisma.users.update : isInitialSetupComplete 값을 true로 업데이트하여 초기 설정 완료 표시

    return res.status(200).json({ message: "Initial setup complete" });
  } catch (error) {
    console.log("서버에러", error);
    return res.status(500).json({ message: "Internal server error" });
  }
}

데이터베이스에 프로필 정보를 저장하고, 초기 설정 완료 상태를 업데이트합니다.

 

초기 설정 상태 설명

isInitialSetupComplete 상태가 true로 변경되면, 이후 로그인 시 더 이상 초기 설정 페이지가 뜨지 않습니다. 따라서 회원가입 후 초기 설정은 한 번만 수행됩니다.

 


 

요약

  1. 프론트엔드
    - 사용자가 입력한 정보를 단계별로 처리하고, 초기 설정 완료 시 서버에 요청을 보냅니다.
    - 초기 설정이 완료되면 isInitialSetupComplete 상태가 true로 변경되어, 이후에는 초기 설정 페이지가 뜨지 않습니다.
  2. 백엔드
    - 초기 설정 정보를 받아 JWT 토큰을 검증한 후, 데이터베이스에 저장하고 초기 설정 완료 상태를 업데이트합니다.