개인정보-컴플라이언스-웹애플리케이션(11) - (자가진단 설문) 프론트 코드

2025. 1. 29. 00:53프로젝트

728x90
반응형

Dashboard.jsx

import React, { useEffect } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { authState } from "../../state/authState";
import {
  systemsState,
  assessmentStatusesState,
  loadingState,
  errorMessageState,
} from "../../state/dashboardState";

function Dashboard() {
  const [systems, setSystems] = useRecoilState(systemsState);
  const [assessmentStatuses, setAssessmentStatuses] = useRecoilState(
    assessmentStatusesState
  );
  const [loading, setLoading] = useRecoilState(loadingState);
  const [errorMessage, setErrorMessage] = useRecoilState(errorMessageState);
  const auth = useRecoilValue(authState);
  const navigate = useNavigate();
  const setAuthState = useSetRecoilState(authState);

  const fetchSystems = async () => {
    setErrorMessage("");
    setLoading(true);
    try {
      console.log("⏳ [FETCH] 시스템 정보 요청 중...");
      const [systemsResponse, statusResponse] = await Promise.all([
        axios.get("http://localhost:3000/systems", { withCredentials: true }),
        axios.get("http://localhost:3000/assessment/status", {
          withCredentials: true,
        }),
      ]);

      console.log("✅ [FETCH] 시스템 응답:", systemsResponse.data);
      console.log("✅ [FETCH] 진단 상태 응답:", statusResponse.data);

      setSystems(systemsResponse.data);
      setAssessmentStatuses(statusResponse.data);
    } catch (error) {
      console.error("❌ 데이터 조회 실패:", error);
      setErrorMessage("데이터를 불러오는 중 오류가 발생했습니다.");
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    if (!auth.isLoggedIn) {
      console.warn(
        "🚨 로그인되지 않은 상태입니다. 로그인 페이지로 이동합니다."
      );
      navigate("/login");
      return;
    }
    fetchSystems();
  }, [auth, navigate]);

  const handleRegisterClick = () => {
    if (!auth.user || !auth.user.id) {
      alert("🚨 사용자 정보가 없습니다. 다시 로그인해주세요.");
      return;
    }
    navigate("/system-register");
  };

  const handleViewResult = (systemId) => {
    console.log("📂 결과 보기 요청:", systemId);
    navigate("/completion", { state: { systemId, userId: auth.user.id } });
  };

  const handleEditResult = (systemId) => {
    console.log("✏️ 수정 요청:", systemId);
    navigate("/SelfTestStart", {
      state: { selectedSystems: [systemId], userInfo: auth.user },
    });
  };

  const handleStartDiagnosis = (systemId) => {
    console.log("🔍 진단 시작 요청:", systemId);
    navigate("/SelfTestStart", {
      state: { selectedSystems: [systemId], userInfo: auth.user },
    });
  };

  const handleLogout = async () => {
    try {
      console.log("🚪 로그아웃 요청 중...");
      const response = await fetch("http://localhost:3000/logout", {
        method: "POST",
        credentials: "include",
      });

      const data = await response.json();

      if (response.ok) {
        console.log("✅ 로그아웃 성공:", data.message);
        alert(data.message);
        setAuthState({
          isLoggedIn: false,
          isExpertLoggedIn: false,
          user: null,
        });
        navigate("/");
      } else {
        console.error("❌ 로그아웃 실패:", data.message);
        alert(data.message || "로그아웃 실패");
      }
    } catch (error) {
      console.error("❌ 로그아웃 요청 오류:", error);
      alert("로그아웃 요청 중 오류가 발생했습니다.");
    }
  };

  return (
    <div className="min-h-screen bg-gray-100">
      <div className="py-6 text-black text-center">
        <h1 className="text-4xl font-bold">기관회원 마이페이지</h1>
      </div>
      <div className="container mx-auto px-4 py-8">
        <div className="flex justify-between items-center mb-8">
          <h2 className="text-2xl font-bold">등록된 시스템</h2>
          <button
            onClick={handleRegisterClick}
            className={`px-4 py-2 font-bold rounded ${
              auth.user
                ? "bg-blue-600 text-white hover:bg-blue-700"
                : "bg-gray-400 text-gray-700 cursor-not-allowed"
            }`}
            disabled={!auth.user}
          >
            시스템 등록
          </button>
        </div>
        {errorMessage && (
          <div className="mb-4 p-4 bg-red-100 text-red-700 border border-red-300 rounded">
            {errorMessage}
          </div>
        )}
        {loading ? (
          <p className="text-center">로딩 중...</p>
        ) : systems.length === 0 ? (
          <p className="text-center">등록된 시스템이 없습니다.</p>
        ) : (
          <div className="grid grid-cols-4 gap-4">
            {systems.map((system) => {
              const isCompleted = assessmentStatuses[system.system_id];
              return (
                <div
                  key={system.system_id}
                  className="p-4 bg-white shadow-lg rounded-md border"
                >
                  <h3 className="font-bold text-lg mb-2">
                    {system.system_name}
                  </h3>
                  {isCompleted ? (
                    <div className="flex flex-col space-y-2">
                      <button
                        onClick={() => handleViewResult(system.system_id)}
                        className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
                      >
                        결과 보기
                      </button>
                      <button
                        onClick={() => handleEditResult(system.system_id)}
                        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
                      >
                        수정하기
                      </button>
                    </div>
                  ) : (
                    <button
                      onClick={() => handleStartDiagnosis(system.system_id)}
                      className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
                    >
                      진단하기
                    </button>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>
      <button
        className="fixed bottom-5 right-5 bg-blue-500 text-white p-4 rounded-full shadow-lg hover:bg-blue-600 w-[100px] h-[100px] flex items-center justify-center flex-col"
        onClick={handleLogout}
      >
        <FontAwesomeIcon icon={faSignOutAlt} size="2xl" />
        <p>로그아웃</p>
      </button>
    </div>
  );
}

export default Dashboard;

 

이 코드는 Recoil, 그리고 Axios를 사용하여 시스템 목록을 표시하고,

사용자와 상호작용할 수 있는 대시보드 페이지를 구현한 것입니다. 아래는 코드의 구성 요소를 자세히 분석한 내용입니다.

 

1. 전역 상태

const [systems, setSystems] = useRecoilState(systemsState);
const [assessmentStatuses, setAssessmentStatuses] = useRecoilState(
  assessmentStatusesState
);
const [loading, setLoading] = useRecoilState(loadingState);
const [errorMessage, setErrorMessage] = useRecoilState(errorMessageState);
const auth = useRecoilValue(authState);

 

systemsState: 등록된 시스템 목록을 관리하는 상태 값입니다.


setSystems를 사용하여 서버에서 가져온 데이터를 업데이트합니다.


assessmentStatusesState:각 시스템의 진단 완료 상태를 관리하는 상태 값입니다.


loadingState:데이터를 가져오는 중인지를 나타내는 상태 값입니다.


errorMessageState:데이터를 가져오는 중 발생한 오류 메시지를 저장하는 상태값입니다,


authState:사용자 로그인 상태 및 정보를 관리하는 전역 상태값입니다.

 

 

2. 데이터 가져오기

const fetchSystems = async () => {
  setErrorMessage("");
  setLoading(true);
  try {
    console.log("⏳ [FETCH] 시스템 정보 요청 중...");
    const [systemsResponse, statusResponse] = await Promise.all([
      axios.get("http://localhost:3000/systems", { withCredentials: true }),
      axios.get("http://localhost:3000/assessment/status", {
        withCredentials: true,
      }),
    ]);

    console.log("✅ [FETCH] 시스템 응답:", systemsResponse.data);
    console.log("✅ [FETCH] 진단 상태 응답:", statusResponse.data);

    setSystems(systemsResponse.data);
    setAssessmentStatuses(statusResponse.data);
  } catch (error) {
    console.error("❌ 데이터 조회 실패:", error);
    setErrorMessage("데이터를 불러오는 중 오류가 발생했습니다.");
  } finally {
    setLoading(false);
  }
};

 

 

setErrorMessage(""): 이전 오류 메시지를 초기화.


setLoading(true): 데이터를 로딩 중임을 표시.


Promise.all: 두 개의 API 요청을 병렬로 실행: 시스템 목록 (GET /systems), 진단 상태 (GET /assessment/status).

 

응답 처리: setSystems: 시스템 목록 데이터를 상태에 저장, setAssessmentStatuses: 진단 상태 데이터를 상태에 저장.

 

오류 처리: 오류 발생 시 setErrorMessage로 오류 메시지 설정.

 

로딩 완료: setLoading(false).

 

진단하기

const handleStartDiagnosis = (systemId) => {
  console.log("🔍 진단 시작 요청:", systemId);
  navigate("/SelfTestStart", {
    state: { selectedSystems: [systemId], userInfo: auth.user },
  });
};

 

새로 진단을 시작하기 위해 /SelfTestStart로 이동.

 

로그아웃

const handleLogout = async () => {
  try {
    console.log("🚪 로그아웃 요청 중...");
    const response = await fetch("http://localhost:3000/logout", {
      method: "POST",
      credentials: "include",
    });

    const data = await response.json();

    if (response.ok) {
      console.log("✅ 로그아웃 성공:", data.message);
      alert(data.message);
      setAuthState({
        isLoggedIn: false,
        isExpertLoggedIn: false,
        user: null,
      });
      navigate("/");
    } else {
      console.error("❌ 로그아웃 실패:", data.message);
      alert(data.message || "로그아웃 실패");
    }
  } catch (error) {
    console.error("❌ 로그아웃 요청 오류:", error);
    alert("로그아웃 요청 중 오류가 발생했습니다.");
  }
};

 

POST /logout 요청으로 세션 삭제.
요청 성공 시

authState 초기화합니다
메인 페이지(/)로 이동합니다.
요청 실패 시 오류 메시지 출력합니다.

 

SelfTestStart.jsx

import React, { useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import axios from "axios";
import { useRecoilState, useRecoilValue } from "recoil";
import { selfTestFormState } from "../../state/selfTestState";
import { authState } from "../../state/authState";

function SelfTestStart() {
  const navigate = useNavigate();
  const location = useLocation();
  const { selectedSystems } = location.state || {};
  const [formData, setFormData] = useRecoilState(selfTestFormState); // 전역 상태 관리
  const auth = useRecoilValue(authState); // 사용자 정보 가져오기

  const systemId =
    selectedSystems && selectedSystems.length > 0 ? selectedSystems[0] : null;
  const userId = auth.user?.id || null;

  useEffect(() => {
    if (!systemId) {
      console.error("시스템 정보가 누락되었습니다.");
    }
    if (!userId) {
      console.error("유저 정보가 누락되었습니다. 다시 로그인해주세요.");
    }
  }, [systemId, userId]);

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFormData((prevData) => ({ ...prevData, [name]: value }));
  };

  const handleButtonClick = (name, value) => {
    setFormData((prevData) => ({ ...prevData, [name]: value }));
  };

  const validateForm = () => {
    for (const [key, value] of Object.entries(formData)) {
      if (!value) {
        console.error(`${key}을(를) 선택해주세요.`);
        return false;
      }
    }
    if (!systemId) {
      console.error("시스템 정보가 누락되었습니다. 다시 선택해주세요.");
      return false;
    }
    if (!userId) {
      console.error("유저 정보가 누락되었습니다. 다시 로그인해주세요.");
      return false;
    }
    return true;
  };

  const handleDiagnosisClick = async (e) => {
    e.preventDefault();

    if (!validateForm()) return;

    try {
      const response = await axios.post(
        "http://localhost:3000/selftest",
        { ...formData, systemId, userId },
        { withCredentials: true }
      );

      console.log("서버 응답:", response.data);
      navigate("/DiagnosisPage", {
        state: { systemId, userId },
      });
    } catch (error) {
      console.error("서버 저장 실패:", error.response?.data || error.message);
    }
  };

  return (
    <div className="bg-gray-100 min-h-screen">
      <div className="container mx-auto max-w-5xl p-6 bg-white rounded-lg shadow-lg">
        <div className="text-center mb-8">
          <h1 className="text-2xl font-bold text-gray-800">자가진단 입력</h1>
        </div>

        <form>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
            <div>
              <label
                htmlFor="organization"
                className="block text-sm font-medium text-gray-700"
              >
                공공기관 분류
              </label>
              <select
                id="organization"
                name="organization"
                value={formData.organization}
                onChange={handleInputChange}
                className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
              >
                <option value="교육기관">교육기관</option>
                <option value="공공기관">공공기관</option>
                <option value="국가기관">국가기관</option>
              </select>
            </div>
            <div>
              <label
                htmlFor="userGroup"
                className="block text-sm font-medium text-gray-700"
              >
                이용자 구분
              </label>
              <select
                id="userGroup"
                name="userGroup"
                value={formData.userGroup}
                onChange={handleInputChange}
                className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
              >
                <option value="1~4명">1~4명</option>
                <option value="5~10명">5~10명</option>
                <option value="10명 이상">10명 이상</option>
              </select>
            </div>
          </div>
          <div className="space-y-4">
            {[
              { label: "개인정보보호 시스템", name: "personalInfoSystem" },
              { label: "회원정보 홈페이지 여부", name: "memberInfoHomepage" },
              { label: "외부정보 제공 여부", name: "externalDataProvision" },
              {
                label: "CCTV 운영 여부",
                name: "cctvOperation",
                options: ["운영", "미운영"],
              },
              { label: "업무 위탁 여부", name: "taskOutsourcing" },
              { label: "개인정보 폐기 여부", name: "personalInfoDisposal" },
            ].map((item) => (
              <div
                key={item.name}
                className="flex items-center justify-between"
              >
                <span className="text-gray-700 font-medium">{item.label}</span>
                <div className="space-x-4">
                  {(item.options || ["있음", "없음"]).map((option) => (
                    <button
                      key={option}
                      type="button"
                      className={`px-4 py-2 rounded-md ${
                        formData[item.name] === option
                          ? "bg-blue-500 text-white"
                          : "bg-gray-300 text-gray-700"
                      }`}
                      onClick={() => handleButtonClick(item.name, option)}
                    >
                      {option}
                    </button>
                  ))}
                </div>
              </div>
            ))}
          </div>
          <div className="mt-8 text-center">
            <button
              onClick={handleDiagnosisClick}
              className="px-6 py-2 bg-blue-600 text-white rounded-md shadow hover:bg-blue-700"
            >
              자가진단하기
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

export default SelfTestStart;

 

이 코드는 사용자가 특정 시스템에 대한 자가진단(SelfTest) 데이터를 입력하고, 이를 서버로 전송하여 저장하는컴포넌트입니다. Recoil을 사용하여 상태를 관리하며, React Router를 사용해 페이지를 전환합니다.

 

주요 변수와 데이터

const { selectedSystems } = location.state || {};
const [formData, setFormData] = useRecoilState(selfTestFormState);
const auth = useRecoilValue(authState);

 

selectedSystems
이전 페이지에서 선택된 시스템 ID 배열을 전달받습니다
기본적으로 첫 번째 시스템을 systemId로 사용합니다
formData
자가진단 입력 데이터. Recoil 전역 상태로 관리되며, 각 입력값이 저장됩니다.
auth
현재 로그인된 사용자 정보. user.id를 통해 사용자 ID를 확인합니다.

 

데이터 핸들링 함수

1. handleInputChange

const handleInputChange = (e) => {
  const { name, value } = e.target;
  setFormData((prevData) => ({ ...prevData, [name]: value }));
};

 

역할: <select> 요소에서 선택된 값을 상태에 저장합니다.
사용 예시: 기관 분류와 이용자 구분합니다.

 

2. handleButtonClick

const handleButtonClick = (name, value) => {
  setFormData((prevData) => ({ ...prevData, [name]: value }));
};

 

역할: 버튼 클릭 시 해당 데이터를 formData에 저장합니다.
사용 예시: 있음 또는 없음 버튼 클릭 시 상태 업데이트합니다.

 

3. validateForm

const validateForm = () => {
  for (const [key, value] of Object.entries(formData)) {
    if (!value) {
      console.error(`${key}을(를) 선택해주세요.`);
      return false;
    }
  }
  if (!systemId || !userId) {
    console.error("시스템 정보 또는 유저 정보가 누락되었습니다.");
    return false;
  }
  return true;
};

 

역할: 모든 입력값이 채워졌는지 검증하고, systemId와 userId가 존재하는지 확인합니다.
목적: 유효성 검사를 통과하지 못하면 서버 요청 방지합니다.

 

4. handleDiagnosisClick

const handleDiagnosisClick = async (e) => {
  e.preventDefault();

  if (!validateForm()) return;

  try {
    const response = await axios.post(
      "http://localhost:3000/selftest",
      { ...formData, systemId, userId },
      { withCredentials: true }
    );

    console.log("서버 응답:", response.data);
    navigate("/DiagnosisPage", { state: { systemId, userId } });
  } catch (error) {
    console.error("서버 저장 실패:", error.response?.data || error.message);
  }
};

 

역할: 유효성 검사를 먼저 실행하고, 서버로 자가진단 데이터를 전송하고, 전송 성공 시 진단 결과 페이지로 이동합니다.
에러 처리: 서버 요청 실패 시 에러 메시지를 콘솔에 출합니다.