개인정보-컴플라이언스-웹애플리케이션(9) - (회원가입 로그인) 프론트 코드

2025. 1. 28. 19:43프로젝트

728x90

우선 기본적으로 회원가입과 로그인 부분의 프론트 부분을 살펴 보겠습니다.

 

우선 회원가입 프론트 부분입니다.

import React, { useState } from "react";
import { useRecoilState } from "recoil";
import { useNavigate } from "react-router-dom";
import { formState } from "../../state/formState";
import SignupStep0 from "../../components/Login/SignupStep0";
import SignupStep1 from "../../components/Login/SignupStep1";
import SignupStep2 from "../../components/Login/SignupStep2";
import SignupStep3 from "../../components/Login/SignupStep3";
import { useResetRecoilState } from "recoil";
import { useEffect } from "react";

function Signup() {
  const [step, setStep] = useState(0); // 현재 단계
  const navigate = useNavigate();
  const [formData, setFormData] = useRecoilState(formState);
  const resetFormState = useResetRecoilState(formState);

  useEffect(() => {
    // 컴포넌트가 언마운트될 때 formState 초기화
    return () => {
      resetFormState();
    };
  }, [resetFormState]);

  const nextStep = () => setStep(step + 1);
  const prevStep = () => setStep(step - 1);

  const handleSubmit = async () => {
    if (!formData.emailVerified) {
      alert("이메일 인증이 필요합니다.");
      return;
    }

    if (!formData.member_type) {
      alert("회원 유형을 선택해 주세요.");
      return;
    }

    const endpoint =
      formData.member_type === "user"
        ? "http://localhost:3000/register"
        : "http://localhost:3000/register/expert";

    const payload = {
      ...formData[formData.member_type], // 선택된 회원 유형의 데이터만 포함
      email: formData.email,
      password: formData.password,
      role: formData.member_type, // 백엔드에서 role을 명확하게 전달하기 위해 추가
    };

    console.log("Payload being sent:", payload);

    try {
      const response = await fetch(endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(payload),
      });

      const data = await response.json();
      console.log("Response received:", data);

      if (response.ok) {
        alert(data.message || "회원가입 성공");
        navigate("/");
      } else {
        alert(data.message || "회원가입 실패");
      }
    } catch (error) {
      console.error("Error during signup:", error.message);
      alert("회원가입 요청 중 오류가 발생했습니다.");
    }
  };

  const renderStep = () => {
    switch (step) {
      case 0:
        return <SignupStep0 nextStep={nextStep} />;
      case 1:
        return <SignupStep1 nextStep={nextStep} />;
      case 2:
        return <SignupStep2 nextStep={nextStep} prevStep={prevStep} />;
      case 3:
        return <SignupStep3 prevStep={prevStep} handleSubmit={handleSubmit} />;
      default:
        return null;
    }
  };

  return (
    <div className="min-h-screen flex flex-col justify-center items-center bg-gray-100">
      {renderStep()}
    </div>
  );
}

export default Signup;

 

리코일 코드 ( formstate )

import { atom } from "recoil";

export const formState = atom({
  key: "formState",
  default: {
    agreement: false, // 추가된 필드
    member_type: "", // "user" 또는 "expert"
    email: "", // 추가된 필드
    password: "", // 추가된 필드
    emailVerified: false, // 추가된 필드
    user: {
      institution_name: "", // 추가된 필드
      institution_address: "", // 추가된 필드
      representative_name: "", // 추가된 필드
      phone_number: "", // ✅ 반드시 phone_number로 유지
    },
    expert: {
      name: "", // 추가된 필드
      institution_name: "", // 추가된 필드
      ofcps: "", // 추가된 필드
      phone_number: "", // 추가된 필드
      major_carrea: "", // 추가된 필드
    },
    name: "",
    min_subjects: "",
    max_subjects: "",
    purpose: "",
    is_private: "포함", // 기본값 설정
    is_unique: "미포함", // 기본값 설정
    is_resident: "포함", // 기본값 설정
    reason: "동의", // 기본값 설정
  },
});

 

const [step, setStep] = useState(0); // 현재 단계
const navigate = useNavigate();
const [formData, setFormData] = useRecoilState(formState);
const resetFormState = useResetRecoilState(formState);

 

현재 회원가입 단계(0~3) 스탭으로 나눠서 회원가입을 구성하였습니다.
useState로 관리하며 setStep으로 단계 이동.
formData: Recoil의 formState를 사용해 입력 데이터를 관리합니다.
전역적으로 사용 가능하며, 다른 컴포넌트에서도 접근 및 수정 가능.
resetFormState: 회원가입 과정에서 상태를 초기화하기 위해 Recoil의 useResetRecoilState 사용합니다.

 

useEffect(() => {
  // 컴포넌트가 언마운트될 때 formState 초기화
  return () => {
    resetFormState();
  };
}, [resetFormState]);

 

컴포넌트가 언마운트될 때(Signup 페이지에서 떠날 때) formState를 초기화합니다.
목적: 다음 회원가입 요청 시 이전 데이터가 남아있지 않도록 상태를 리셋합니다.

 

const handleSubmit = async () => {
  if (!formData.emailVerified) {
    alert("이메일 인증이 필요합니다.");
    return;
  }

  if (!formData.member_type) {
    alert("회원 유형을 선택해 주세요.");
    return;
  }

  const endpoint =
    formData.member_type === "user"
      ? "http://localhost:3000/register"
      : "http://localhost:3000/register/expert";

  const payload = {
    ...formData[formData.member_type],
    email: formData.email,
    password: formData.password,
    role: formData.member_type,
  };

  console.log("Payload being sent:", payload);

  try {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    const data = await response.json();
    console.log("Response received:", data);

    if (response.ok) {
      alert(data.message || "회원가입 성공");
      navigate("/");
    } else {
      alert(data.message || "회원가입 실패");
    }
  } catch (error) {
    console.error("Error during signup:", error.message);
    alert("회원가입 요청 중 오류가 발생했습니다.");
  }
};

 

(1) 입력 검증
emailVerified: 이메일 인증이 완료되지 않았으면 경고 메시지 출력 후 종료.
member_type: "사용자" 또는 "전문가" 유형이 선택되지 않았으면 경고 메시지 출력 후 종료.

 

(2) 백엔드 API 요청 준비
endpoint: 회원 유형(user or expert)에 따라 다른 API URL 선택: 사용자: /register ,전문가: /register/expert
payload: 사용자 입력 데이터를 포함한 요청 본문(JSON).

 

(3) API 요청 fetch 사용: POST 요청으로 회원가입 데이터를 서버에 전송.
성공 응답 처리: 서버 응답이 성공적이면 성공 메시지를 표시하고 메인 페이지(/)로 이동.
실패 응답 처리: 실패 메시지를 표시하며 오류를 로그로 출력.

 

로그인 부분 

import React, { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import { useSetRecoilState } from "recoil";
import {
  authState,
  expertAuthState,
  superUserAuthState,
} from "../../state/authState";

function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [userType, setUserType] = useState("user"); // "user", "expert", "superuser"
  const [errorMessage, setErrorMessage] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);
  const navigate = useNavigate();
  const setAuthState = useSetRecoilState(authState);
  const setExpertAuthState = useSetRecoilState(expertAuthState);
  const setSuperUserAuthState = useSetRecoilState(superUserAuthState);

  const handleLogin = async () => {
    if (!email || !password) {
      setErrorMessage("이메일과 비밀번호를 입력해 주세요.");
      return;
    }
    setIsSubmitting(true);

    const endpoint =
      userType === "user"
        ? "http://localhost:3000/login"
        : userType === "expert"
        ? "http://localhost:3000/login/expert"
        : "http://localhost:3000/login/superuser";

    try {
      console.log("🚀 [LOGIN] 요청 전송:", endpoint, { email, password });
      const response = await axios.post(
        endpoint,
        { email, password },
        { withCredentials: true }
      );
      console.log("✅ [LOGIN] 응답 데이터:", response.data);

      const { id, member_type, ...userData } = response.data.data;

      // 사용자 유형별로 상태 업데이트
      if (member_type === "superuser") {
        setSuperUserAuthState({
          isLoggedIn: true,
          user: { id, member_type, ...userData },
        });
        navigate("/superuserpage");
      } else if (member_type === "expert") {
        setExpertAuthState({
          isLoggedIn: true,
          user: { id, member_type, ...userData },
        });
        navigate("/system-management");
      } else {
        setAuthState({
          isLoggedIn: true,
          user: { id, member_type, ...userData },
        });
        navigate("/dashboard");
      }
    } catch (error) {
      console.error("❌ [LOGIN] 오류:", error.response?.data || error.message);
      setErrorMessage(
        error.response?.data?.msg || "로그인 요청 중 문제가 발생했습니다."
      );
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="min-h-screen flex flex-col justify-center items-center bg-gray-100">
      <div className="bg-white p-6 rounded-lg shadow-md w-3/4 max-w-md">
        <h1 className="text-2xl font-bold mb-6">로그인</h1>
        <div className="space-y-4">
          {/* 회원 유형 선택 */}
          <div>
            <label className="block text-gray-700 font-medium mb-2">
              회원 유형
            </label>
            <select
              value={userType}
              onChange={(e) => setUserType(e.target.value)}
              className="w-full p-3 border border-gray-300 rounded-md"
            >
              <option value="user">일반회원</option>
              <option value="expert">관리자</option>
              <option value="superuser">슈퍼유저</option>
            </select>
          </div>

          {/* 이메일 입력 */}
          <div>
            <label className="block text-gray-700 font-medium">이메일</label>
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="w-full p-3 border border-gray-300 rounded-md"
              placeholder="이메일을 입력하세요"
            />
          </div>

          {/* 비밀번호 입력 */}
          <div>
            <label className="block text-gray-700 font-medium">비밀번호</label>
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              className="w-full p-3 border border-gray-300 rounded-md"
              placeholder="비밀번호를 입력하세요"
            />
          </div>

          {/* 오류 메시지 */}
          {errorMessage && (
            <p className="text-red-500 text-center">{errorMessage}</p>
          )}

          {/* 로그인 버튼 */}
          <button
            onClick={handleLogin}
            className={`w-full px-4 py-3 font-bold rounded-md ${
              isSubmitting
                ? "bg-gray-300 text-gray-700 cursor-not-allowed"
                : "bg-blue-600 text-white hover:bg-blue-700"
            }`}
            disabled={isSubmitting}
          >
            {isSubmitting ? "로그인 중..." : "로그인"}
          </button>
        </div>
      </div>
    </div>
  );
}

export default Login;
import { atom } from "recoil";

// 일반 사용자 로그인 상태
export const authState = atom({
  key: "authState",
  default: {
    isLoggedIn: false, // 로그인 상태
    user: null, // 로그인된 사용자 정보
  },
});

// 관리자 로그인 상태
export const expertAuthState = atom({
  key: "expertAuthState",
  default: {
    isLoggedIn: false, // 관리자 로그인 여부
    user: null, // 로그인된 관리자 정보
  },
});

// 슈퍼유저 로그인 상태
export const superUserAuthState = atom({
  key: "superUserAuthState",
  default: {
    isLoggedIn: false, // 슈퍼유저 로그인 여부
    user: null, // 로그인된 슈퍼유저 정보
  },
});

 

이 코드는 Recoil을 사용해 구현된 로그인 페이지입니다. 사용자는 이메일, 비밀번호, 그리고 회원 유형(일반회원, 관리자, 슈퍼유저)을 입력하여 로그인할 수 있습니다. 각 입력값을 검증하며, 성공적인 로그인이 이루어지면 사용자 유형에 따라 적절한 페이지로 이동합니다.

 

1. 컴포넌트 상태 및 초기화

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [userType, setUserType] = useState("user"); // 기본값: 일반회원
const [errorMessage, setErrorMessage] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);

 

 

email: 사용자가 입력한 이메일 주소입니다.

password: 사용자가 입력한 비밀번호입니다.

userType: 선택한 회원 유형 (user, expert, superuser). 기본값은 user입니다.

errorMessage: 로그인 실패 시 오류 메시지를 표시하기 위한 상태입니다.

isSubmitting: 로그인 요청 중인지 나타내는 상태. 요청 중에는 버튼이 비활성화되어 중복 클릭을 방지합니다.

 

2. Recoil 상태 관리

const setAuthState = useSetRecoilState(authState);
const setExpertAuthState = useSetRecoilState(expertAuthState);
const setSuperUserAuthState = useSetRecoilState(superUserAuthState);

 

 

authState: 일반회원 로그인 상태를 관리합니다.
expertAuthState: 관리자 로그인 상태를 관리합니다.
superUserAuthState: 슈퍼유저 로그인 상태를 관리합니다.
로그인 성공 시, 각 회원 유형에 따라 적절한 상태를 업데이트하여 전역 상태를 관리합니다.

 

3. handleLogin 함수

const handleLogin = async () => {
  if (!email || !password) {
    setErrorMessage("이메일과 비밀번호를 입력해 주세요.");
    return;
  }
  setIsSubmitting(true);

 

입력값 검증
이메일과 비밀번호가 비어 있으면 오류 메시지를 표시하고 종료.

 

3.1. 요청할 API 엔드포인트 결정

const endpoint =
  userType === "user"
    ? "http://localhost:3000/login"
    : userType === "expert"
    ? "http://localhost:3000/login/expert"
    : "http://localhost:3000/login/superuser";

 

선택한 회원 유형에 따라 로그인 요청을 보낼 API 엔드포인트를 결정합니다.
일반회원은 /login
관리자는 /login/expert
슈퍼유저는 /login/superuser

 

3.2. 로그인 요청

try {
  console.log("🚀 [LOGIN] 요청 전송:", endpoint, { email, password });
  const response = await axios.post(
    endpoint,
    { email, password },
    { withCredentials: true }
  );
  console.log("✅ [LOGIN] 응답 데이터:", response.data);

 

xios.post: 이메일과 비밀번호를 요청 본문으로 전달합니다.
withCredentials: true: 브라우저가 서버에 세션 쿠키를 포함하도록 설정합니다.
로그 요청 정보: 요청이 전송되기 전과 응답을 받은 후 디버깅 정보를 출력합니다.

 

3.3. 로그인 성공 처리

const { id, member_type, ...userData } = response.data.data;

// 사용자 유형별로 상태 업데이트
if (member_type === "superuser") {
  setSuperUserAuthState({
    isLoggedIn: true,
    user: { id, member_type, ...userData },
  });
  navigate("/superuserpage");
} else if (member_type === "expert") {
  setExpertAuthState({
    isLoggedIn: true,
    user: { id, member_type, ...userData },
  });
  navigate("/system-management");
} else {
  setAuthState({
    isLoggedIn: true,
    user: { id, member_type, ...userData },
  });
  navigate("/dashboard");
}

 

회원 유형별 상태 업데이트 및 페이지 이동합니다.
슈퍼유저(superuser): /superuserpage로 이동.
관리자(expert): /system-management로 이동.
일반회원(user): /dashboard로 이동.

 

4.1. 회원 유형 선택

<select
  value={userType}
  onChange={(e) => setUserType(e.target.value)}
  className="w-full p-3 border border-gray-300 rounded-md"
>
  <option value="user">일반회원</option>
  <option value="expert">관리자</option>
  <option value="superuser">슈퍼유저</option>
</select>

 

 

드롭다운 메뉴로 회원 유형을 선택합니다.

선택한 값은 userType 상태에 저장합니다.

 

4.2. 이메일 및 비밀번호 입력

<input
  type="email"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
  className="w-full p-3 border border-gray-300 rounded-md"
  placeholder="이메일을 입력하세요"
/>
<input
  type="password"
  value={password}
  onChange={(e) => setPassword(e.target.value)}
  className="w-full p-3 border border-gray-300 rounded-md"
  placeholder="비밀번호를 입력하세요"
/>

 

 

사용자가 입력한 이메일과 비밀번호를 각각 email, password 상태에 저장합니다.

 

4.3. 로그인 버튼

<button
  onClick={handleLogin}
  className={`w-full px-4 py-3 font-bold rounded-md ${
    isSubmitting
      ? "bg-gray-300 text-gray-700 cursor-not-allowed"
      : "bg-blue-600 text-white hover:bg-blue-700"
  }`}
  disabled={isSubmitting}
>
  {isSubmitting ? "로그인 중..." : "로그인"}
</button>