개인정보-컴플라이언스-웹애플리케이션(12) - (자가진단 정량문항,정성문항) 프론트 코드
DiagnosisPage.jsx
import React, { useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import axios from "axios";
import { useRecoilState } from "recoil";
import {
quantitativeDataState,
responsesState,
currentStepState,
} from "../../state/selfTestState";
function DiagnosisPage() {
const navigate = useNavigate();
const location = useLocation();
const { userId, systemId } = location.state || {};
const [quantitativeData, setQuantitativeData] = useRecoilState(
quantitativeDataState
);
const [responses, setResponses] = useRecoilState(responsesState);
const [currentStep, setCurrentStep] = useRecoilState(currentStepState);
useEffect(() => {
if (!userId || !systemId) {
console.error("Missing userId or systemId:", { userId, systemId });
alert("시스템 또는 사용자 정보가 누락되었습니다. 대시보드로 이동합니다.");
navigate("/dashboard");
return;
}
const fetchQuantitativeData = async () => {
try {
const response = await axios.get(
"http://localhost:3000/selftest/quantitative",
{ params: { systemId }, withCredentials: true }
);
const data = response.data.filter(
(item) => item.question_number >= 1 && item.question_number <= 43
);
setQuantitativeData(data);
const initialResponses = data.reduce((acc, item) => {
acc[item.question_number] = {
response: item.response || "",
additionalComment: item.additional_comment || "",
};
return acc;
}, {});
setResponses(initialResponses);
console.log("Initialized Responses:", initialResponses);
} catch (error) {
console.error("Error fetching quantitative data:", error);
alert("정량 데이터를 불러오는 데 실패했습니다. 다시 시도해주세요.");
}
};
fetchQuantitativeData();
}, [userId, systemId, navigate, setQuantitativeData, setResponses]);
const validateResponses = (data) => {
return data.every(
(item) =>
item.questionNumber &&
item.response &&
item.systemId &&
typeof item.questionNumber === "number"
);
};
const saveAllResponses = async () => {
const requestData = Array.from({ length: 43 }, (_, index) => {
const questionNumber = index + 1;
return {
questionNumber, // 문항 번호
response: responses[questionNumber]?.response || "N/A", // 기본값 설정
additionalComment:
responses[questionNumber]?.additionalComment || "추가 의견 없음", // 기본값 설정
systemId,
};
});
console.log("Sending quantitative responses:", requestData); // 디버깅용
try {
await axios.post(
"http://localhost:3000/selftest/quantitative",
{ quantitativeResponses: requestData },
{ withCredentials: true }
);
alert("모든 응답이 저장되었습니다.");
navigate("/qualitative-survey", { state: { systemId, userId } });
} catch (error) {
console.error(
"Error saving all responses:",
error.response?.data || error
);
alert(
error.response?.data?.message ||
"응답 저장 중 오류가 발생했습니다. 다시 시도해주세요."
);
}
};
const handleNextClick = async () => {
if (currentStep < 43) {
setCurrentStep((prev) => prev + 1);
} else {
await saveAllResponses();
}
};
const handlePreviousClick = () => {
if (currentStep > 1) setCurrentStep((prev) => prev - 1);
};
const renderCurrentStep = () => {
const currentData = quantitativeData.find(
(item) => item.question_number === currentStep
) || {
question_number: currentStep,
unit: "",
evaluation_method: "",
score: "",
question: "질문 없음",
legal_basis: "",
criteria_and_references: "",
response: "",
additional_comment: "",
feedback: "",
};
return (
<table className="w-full border-collapse border border-gray-300 mb-6">
<tbody>
<tr>
<td className="bg-gray-200 p-2 border">지표 번호</td>
<td className="p-2 border">{currentData.question_number}</td>
<td className="bg-gray-200 p-2 border">단위</td>
<td className="p-2 border">{currentData.unit || "N/A"}</td>
</tr>
<tr>
<td className="bg-gray-200 p-2 border">평가방법</td>
<td className="p-2 border">
{currentData.evaluation_method || "N/A"}
</td>
<td className="bg-gray-200 p-2 border">배점</td>
<td className="p-2 border">{currentData.score || "N/A"}</td>
</tr>
<tr>
<td className="bg-gray-200 p-2 border">지표</td>
<td colSpan="3" className="p-2 border">
{currentData.question}
</td>
</tr>
<tr>
<td className="bg-gray-200 p-2 border">근거법령</td>
<td colSpan="3" className="p-2 border">
{currentData.legal_basis || "N/A"}
</td>
</tr>
<tr>
<td className="bg-gray-200 p-2 border">평가기준 (착안 사항)</td>
<td colSpan="3" className="p-2 border">
{currentData.criteria_and_references || "N/A"}
</td>
</tr>
<tr>
<td className="bg-gray-200 p-2 border">파일첨부</td>
<td colSpan="3" className="p-2 border">
<input type="file" className="w-full p-1 border rounded" />
</td>
</tr>
<tr>
<td className="bg-gray-200 p-2 border">평가</td>
<td colSpan="3" className="p-2 border">
<select
value={responses[currentStep]?.response || ""}
onChange={(e) =>
setResponses((prev) => ({
...prev,
[currentStep]: {
...prev[currentStep],
response: e.target.value,
},
}))
}
className="w-full p-2 border border-gray-300 rounded-md"
>
<option value="">선택</option>
<option value="이행">이행</option>
<option value="미이행">미이행</option>
<option value="해당없음">해당없음</option>
<option value="자문 필요">자문 필요</option>
</select>
</td>
</tr>
{responses[currentStep]?.response === "자문 필요" && (
<tr>
<td className="bg-gray-200 p-2 border">자문 필요 사항</td>
<td colSpan="3" className="p-2 border">
<textarea
value={responses[currentStep]?.additional_comment || ""}
onChange={(e) =>
setResponses((prev) => ({
...prev,
[currentStep]: {
...prev[currentStep],
additional_comment: e.target.value,
},
}))
}
className="w-full p-2 border border-gray-300 rounded-md"
placeholder="추가 의견을 입력하세요"
></textarea>
</td>
</tr>
)}
<tr>
<td className="bg-gray-200 p-2 border">피드백</td>
<td colSpan="3" className="p-2 border">
{currentData.feedback || "N/A"}
</td>
</tr>
</tbody>
</table>
);
};
return (
<div className="bg-gray-100 min-h-screen flex flex-col items-center">
<div className="container mx-auto max-w-5xl bg-white mt-10 p-6 rounded-lg shadow-lg">
<h2 className="text-xl font-bold mb-6">정량 설문조사</h2>
{renderCurrentStep()}
<div className="flex justify-between mt-6">
<button
onClick={handlePreviousClick}
disabled={currentStep === 1}
className="px-6 py-2 bg-gray-400 text-white rounded-md shadow hover:bg-gray-500"
>
이전
</button>
<button
onClick={handleNextClick}
className="px-6 py-2 bg-blue-600 text-white rounded-md shadow hover:bg-blue-700"
>
{currentStep === 43 ? "완료" : "다음"}
</button>
</div>
</div>
</div>
);
}
export default DiagnosisPage;
DiagnosisPage 컴포넌트는 사용자가 특정 시스템에 대한 정량 설문조사를 진행하고, 데이터를 저장하는 역할을 합니다. 데이터를 서버에서 불러와 입력하며, 응답을 저장한 후 정성 설문조사 페이지(QualitativeSurvey.jsx)로 이동합니다.
1. 주요 기능 요약
✅ 서버에서 정량 설문 데이터를 가져옴
✅ 43개의 설문 문항을 하나씩 표시하고, 응답을 저장
✅ 사용자가 모든 문항을 완료하면 서버에 저장 후 다음 페이지 이동
✅ 각 문항별로 ‘이행’, ‘미이행’ 등의 응답을 선택하고 저장
✅ ‘자문 필요’ 선택 시 추가 의견을 입력할 수 있음
2 - 1. 주요 상태 및 변수
const navigate = useNavigate();
const location = useLocation();
const { userId, systemId } = location.state || {};
navigate: 페이지 이동을 위한 React Router Hook.
location.state: 이전 페이지에서 전달된 userId와 systemId를 가져옵니다.
2 - 2 Recoil 상태
const [quantitativeData, setQuantitativeData] = useRecoilState(quantitativeDataState);
const [responses, setResponses] = useRecoilState(responsesState);
const [currentStep, setCurrentStep] = useRecoilState(currentStepState);
quantitativeData: 서버에서 가져온 설문 데이터 리스트를 저장합니다.
responses: 사용자의 응답을 저장하는 객체. {question_number: { response, additionalComment }} 형태로 저장됩니다.
currentStep: 현재 진행 중인 문항 번호 (1~43).
3. 서버에서 정량 설문 데이터를 불러오기
const fetchQuantitativeData = async () => {
try {
const response = await axios.get(
"http://localhost:3000/selftest/quantitative",
{ params: { systemId }, withCredentials: true }
);
const data = response.data.filter(
(item) => item.question_number >= 1 && item.question_number <= 43
);
setQuantitativeData(data);
const initialResponses = data.reduce((acc, item) => {
acc[item.question_number] = {
response: item.response || "",
additionalComment: item.additional_comment || "",
};
return acc;
}, {});
setResponses(initialResponses);
} catch (error) {
console.error("Error fetching quantitative data:", error);
alert("정량 데이터를 불러오는 데 실패했습니다. 다시 시도해주세요.");
}
};
서버에서 selftest/quantitative 데이터를 가져옵니다.
question_number가 1~43인 데이터만 필터링하여 사용합니다.
responses 상태를 초기화하여 문항별 응답을 저장할 수 있도록 합니다.
4. 사용자의 응답을 서버에 저장
const saveAllResponses = async () => {
const requestData = Array.from({ length: 43 }, (_, index) => {
const questionNumber = index + 1;
return {
questionNumber,
response: responses[questionNumber]?.response || "N/A",
additionalComment: responses[questionNumber]?.additionalComment || "추가 의견 없음",
systemId,
};
});
try {
await axios.post(
"http://localhost:3000/selftest/quantitative",
{ quantitativeResponses: requestData },
{ withCredentials: true }
);
alert("모든 응답이 저장되었습니다.");
navigate("/qualitative-survey", { state: { systemId, userId } });
} catch (error) {
console.error("Error saving all responses:", error.response?.data || error);
alert("응답 저장 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};
주요 기능
문항별 응답을 객체 배열(requestData)로 변환합니다.
모든 응답을 서버로 POST 요청하여 저장됩니다.
저장 후 정성 설문조사 페이지로 이동합니다.
QualitativeSurvey.jsx
import React, { useEffect } from "react";
import axios from "axios";
import { useNavigate, useLocation } from "react-router-dom";
import { useRecoilState } from "recoil";
import {
qualitativeDataState,
qualitativeResponsesState,
qualitativeCurrentStepState,
} from "../../state/selfTestState";
function QualitativeSurvey() {
const [currentStep, setCurrentStep] = useRecoilState(
qualitativeCurrentStepState
);
const [responses, setResponses] = useRecoilState(qualitativeResponsesState);
const [qualitativeData, setQualitativeData] =
useRecoilState(qualitativeDataState);
const navigate = useNavigate();
const location = useLocation();
const { userId, systemId } = location.state || {};
useEffect(() => {
if (!systemId || !userId) {
console.error("필수 데이터(userId 또는 systemId)가 누락되었습니다.");
alert("시스템 또는 사용자 정보가 누락되었습니다.");
navigate("/dashboard");
return;
}
const fetchQualitativeData = async () => {
try {
const response = await axios.get(
"http://localhost:3000/selftest/qualitative",
{
params: { systemId },
withCredentials: true,
}
);
const data = response.data || [];
setQualitativeData(data);
const initialResponses = data.reduce((acc, item) => {
acc[item.question_number] = {
response: item.response || "해당없음",
additionalComment: item.additional_comment || "",
};
return acc;
}, {});
setResponses(initialResponses);
console.log("Initialized Responses:", initialResponses);
} catch (error) {
console.error(
"정성 문항 데이터를 불러오지 못했습니다:",
error.response || error
);
alert("데이터를 불러오는 중 오류가 발생했습니다.");
navigate("/dashboard");
}
};
fetchQualitativeData();
}, [systemId, userId, navigate, setQualitativeData, setResponses]);
const saveResponse = async (questionNumber) => {
const currentResponse = responses[questionNumber] || {};
if (!systemId || !userId) {
console.error("시스템 또는 사용자 정보가 누락되었습니다.");
alert("시스템 또는 사용자 정보가 누락되었습니다.");
return false;
}
const requestData = {
questionNumber,
response: currentResponse.response || "해당없음",
additionalComment: currentResponse.additionalComment || "",
systemId,
userId,
};
if (
!requestData.questionNumber ||
!requestData.response ||
!requestData.systemId ||
!requestData.userId
) {
console.error("Invalid requestData:", requestData);
alert("필수 데이터가 누락되었습니다. 모든 문항을 확인해주세요.");
return false;
}
try {
await axios.post(
"http://localhost:3000/selftest/qualitative",
requestData,
{ withCredentials: true }
);
console.log(
`Response for question ${questionNumber} saved successfully.`
);
return true;
} catch (error) {
console.error("정성 설문 저장 실패:", error.response?.data || error);
alert("정성 설문 저장 중 오류가 발생했습니다. 다시 시도해주세요.");
return false;
}
};
const handleNextClick = async () => {
const success = await saveResponse(currentStep);
if (!success) return;
if (currentStep < 8) {
setCurrentStep((prev) => prev + 1);
} else {
try {
const response = await axios.post(
"http://localhost:3000/assessment/complete",
{ userId, systemId },
{ withCredentials: true }
);
console.log("최종 결과 저장 성공:", response.data);
alert("결과가 성공적으로 저장되었습니다.");
navigate("/completion", { state: { userId, systemId } });
} catch (error) {
console.error("최종 결과 저장 실패:", error.response?.data || error);
alert("결과 저장 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
const handlePreviousClick = () => {
if (currentStep > 1) setCurrentStep((prev) => prev - 1);
};
const renderCurrentStep = () => {
if (qualitativeData.length === 0) {
return (
<p className="text-center">정성 문항 데이터를 불러오는 중입니다...</p>
);
}
const currentData = qualitativeData.find(
(item) => item.question_number === currentStep
) || {
question_number: currentStep,
indicator: "질문 없음",
indicator_definition: "",
evaluation_criteria: "",
reference_info: "",
};
return (
<table className="w-full border-collapse border border-gray-300 mb-6">
<tbody>
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">
지표 번호
</td>
<td className="border border-gray-300 p-2">{currentStep}</td>
</tr>
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">지표</td>
<td className="border border-gray-300 p-2">
{currentData.indicator}
</td>
</tr>
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">
지표 정의
</td>
<td className="border border-gray-300 p-2">
{currentData.indicator_definition}
</td>
</tr>
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">
평가기준 (착안사항)
</td>
<td className="border border-gray-300 p-2">
{currentData.evaluation_criteria}
</td>
</tr>
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">참고사항</td>
<td className="border border-gray-300 p-2">
{currentData.reference_info}
</td>
</tr>
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">
파일첨부 (선택)
</td>
<td className="border border-gray-300 p-2">
<input
type="file"
className="border border-gray-300 rounded-md w-full p-2"
onChange={(e) =>
setResponses((prev) => ({
...prev,
[currentStep]: {
...prev[currentStep],
filePath: e.target.files[0],
},
}))
}
/>
</td>
</tr>
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">평가</td>
<td className="border border-gray-300 p-2">
<div className="flex items-center space-x-4">
{["자문필요", "해당없음"].map((option) => (
<label key={option} className="flex items-center">
<input
type="radio"
name={`response_${currentStep}`}
value={option}
onChange={(e) =>
setResponses((prev) => ({
...prev,
[currentStep]: {
...prev[currentStep],
response: e.target.value,
},
}))
}
checked={responses[currentStep]?.response === option}
className="mr-2"
/>
{option}
</label>
))}
</div>
</td>
</tr>
{responses[currentStep]?.response === "자문필요" && (
<tr>
<td className="border border-gray-300 p-2 bg-gray-200">
자문 내용
</td>
<td className="border border-gray-300 p-2">
<textarea
placeholder="자문 필요 내용을 입력하세요"
className="w-full p-2 border border-gray-300 rounded-md"
value={responses[currentStep]?.additionalComment || ""}
onChange={(e) =>
setResponses((prev) => ({
...prev,
[currentStep]: {
...prev[currentStep],
additionalComment: e.target.value,
},
}))
}
></textarea>
</td>
</tr>
)}
</tbody>
</table>
);
};
return (
<div className="bg-gray-100 min-h-screen flex flex-col items-center">
<div className="container mx-auto max-w-5xl bg-white mt-10 p-6 rounded-lg shadow-lg">
<h2 className="text-xl font-bold mb-6">
정성 설문조사 ({currentStep}/8번)
</h2>
{renderCurrentStep()}
<div className="flex justify-between mt-6">
<button
onClick={handlePreviousClick}
disabled={currentStep === 1}
className="px-6 py-2 bg-gray-400 text-white rounded-md shadow hover:bg-gray-500"
>
이전
</button>
<button
onClick={handleNextClick}
className="px-6 py-2 bg-blue-600 text-white rounded-md shadow hover:bg-blue-700"
>
{currentStep === 8 ? "완료" : "다음"}
</button>
</div>
</div>
</div>
);
}
export default QualitativeSurvey;
QualitativeSurvey 컴포넌트는 사용자가 특정 시스템에 대한 정성 설문조사를 진행하고 데이터를 저장하는 역할을 합니다. 사용자는 8개의 문항에 대해 응답을 입력하며, 모든 응답이 완료되면 최종 결과를 서버에 저장하고 완료 페이지로 이동합니다.
1. 주요 기능 요약
✅ 서버에서 정성 설문 데이터를 가져옴
✅ 각 문항을 하나씩 표시하고, 응답을 저장
✅ ‘자문 필요’ 선택 시 추가 의견을 입력할 수 있음
✅ 8개 문항을 모두 완료하면 최종 결과 저장 후 완료 페이지로 이동
2 - 1. 주요 상태 및 변수
const navigate = useNavigate();
const location = useLocation();
const { userId, systemId } = location.state || {};
navigate: 페이지 이동을 위한 React Router Hook.
location.state: 이전 페이지에서 전달된 userId와 systemId를 가져옵니다.
2 - 2 Recoil 상태
const [currentStep, setCurrentStep] = useRecoilState(qualitativeCurrentStepState);
const [responses, setResponses] = useRecoilState(qualitativeResponsesState);
const [qualitativeData, setQualitativeData] = useRecoilState(qualitativeDataState);
qualitativeData: 서버에서 가져온 정성 설문 데이터 리스트를 저장합니다.
responses: 사용자의 응답을 저장하는 객체. {question_number: { response, additionalComment }} 형태로 저장됩니다.
currentStep: 현재 진행 중인 문항 번호 (1~8).
3. 서버에서 정성 설문 데이터를 불러오기
const fetchQualitativeData = async () => {
try {
const response = await axios.get(
"http://localhost:3000/selftest/qualitative",
{
params: { systemId },
withCredentials: true,
}
);
const data = response.data || [];
setQualitativeData(data);
const initialResponses = data.reduce((acc, item) => {
acc[item.question_number] = {
response: item.response || "해당없음",
additionalComment: item.additional_comment || "",
};
return acc;
}, {});
setResponses(initialResponses);
console.log("Initialized Responses:", initialResponses);
} catch (error) {
console.error("정성 문항 데이터를 불러오지 못했습니다:", error.response || error);
alert("데이터를 불러오는 중 오류가 발생했습니다.");
navigate("/dashboard");
}
};
주요 기능
서버에서 selftest/qualitative 데이터를 가져옵니다.
가져온 데이터를 qualitativeData 상태에 저장합니다.
responses 상태를 초기화하여 문항별 응답을 저장할 수 있도록 합니다.
4. 사용자의 응답을 서버에 저장
const saveResponse = async (questionNumber) => {
const currentResponse = responses[questionNumber] || {};
if (!systemId || !userId) {
console.error("시스템 또는 사용자 정보가 누락되었습니다.");
alert("시스템 또는 사용자 정보가 누락되었습니다.");
return false;
}
const requestData = {
questionNumber,
response: currentResponse.response || "해당없음",
additionalComment: currentResponse.additionalComment || "",
systemId,
userId,
};
if (!requestData.questionNumber || !requestData.response || !requestData.systemId || !requestData.userId) {
console.error("Invalid requestData:", requestData);
alert("필수 데이터가 누락되었습니다. 모든 문항을 확인해주세요.");
return false;
}
try {
await axios.post("http://localhost:3000/selftest/qualitative", requestData, { withCredentials: true });
console.log(`Response for question ${questionNumber} saved successfully.`);
return true;
} catch (error) {
console.error("정성 설문 저장 실패:", error.response?.data || error);
alert("정성 설문 저장 중 오류가 발생했습니다. 다시 시도해주세요.");
return false;
}
};
주요 기능
현재 응답을 requestData 객체로 생성합니다.
필수 데이터(userId, systemId, questionNumber, response)가 없는 경우 오류가 발생합니다.
서버로 POST 요청을 보내 응답 저장합니다.