2025. 1. 29. 00:53ㆍ프로젝트
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);
}
};
역할: 유효성 검사를 먼저 실행하고, 서버로 자가진단 데이터를 전송하고, 전송 성공 시 진단 결과 페이지로 이동합니다.
에러 처리: 서버 요청 실패 시 에러 메시지를 콘솔에 출합니다.
'프로젝트' 카테고리의 다른 글
개인정보-컴플라이언스-웹애플리케이션(13) - (정량문항 피드백 , 정성문항피드백) 프론트 코드 (0) | 2025.01.29 |
---|---|
개인정보-컴플라이언스-웹애플리케이션(12) - (자가진단 정량문항,정성문항) 프론트 코드 (0) | 2025.01.29 |
개인정보-컴플라이언스-웹애플리케이션(10) - (시스템) 프론트 코드 (0) | 2025.01.28 |
개인정보-컴플라이언스-웹애플리케이션(9) - (회원가입 로그인) 프론트 코드 (0) | 2025.01.28 |
개인정보-컴플라이언스-웹애플리케이션(8) - (피드백) 백엔드 코드 (0) | 2025.01.26 |