교우관계 웹 앱 만들기 - 03.설문조사 페이지 만들기
교우관계 설문조사 페이지 구현
1. 설문조사 페이지 구조
교우관계 설문조사 페이지는 크게 두 부분으로 구성됩니다:
- Canvas를 활용한 인터랙티브 관계도
- 추가 설문 문항 폼
1.1 HTML 구조
<div id="name-input-container">
자신의 이름을 입력하세요:
<input type="text" id="name-input" />
<button id="save-name">저장</button>
</div>
<div id="canvas-container">
<canvas id="relationship-canvas"></canvas>
</div>
<div id="survey-container">
<div class="survey-item">
<label>칭찬하고 싶은 친구와 이유:</label>
<input type="text" id="praise-name" />
<textarea id="praise-reason"></textarea>
</div>
<!-- 추가 설문 항목들 -->
</div>
2. Canvas 관계도 구현
2.1 캔버스 초기화
const canvas = document.getElementById("relationship-canvas");
const ctx = canvas.getContext("2d");
// 캔버스 크기 설정
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 150; // 학생들이 배치될 원의 반지름
2.2 학생 위치 계산
function calculateStudentPositions() {
studentPositions = [];
const angleStep = (2 * Math.PI) / students.length;
students.forEach((student, index) => {
const angle = index * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
studentPositions.push({ x, y });
});
}
2.3 관계도 그리기
function drawStudents() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 중심 원(자신) 그리기
ctx.beginPath();
ctx.arc(centerX, centerY, 25, 0, 2 * Math.PI);
ctx.fillStyle = "#4CAF50";
ctx.fill();
// 자신의 이름 표시
ctx.fillStyle = "black";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(userName, centerX, centerY);
// 다른 학생들 그리기
students.forEach((student, index) => {
const pos = studentPositions[index];
ctx.beginPath();
ctx.arc(pos.x, pos.y, 10, 0, 2 * Math.PI);
ctx.fillStyle = "blue";
ctx.fill();
ctx.fillStyle = "black";
ctx.font = "12px Arial";
ctx.textAlign = "left";
ctx.fillText(student, pos.x + 15, pos.y);
});
}
3. 드래그 앤 드롭 구현
3.1 마우스 이벤트 처리
let dragging = false;
let dragIndex = -1;
canvas.addEventListener("mousedown", (evt) => {
const mousePos = getMousePos(canvas, evt);
studentPositions.forEach((pos, index) => {
if (isInsideCircle(mousePos.x, mousePos.y, pos.x, pos.y, 10)) {
dragging = true;
dragIndex = index;
}
});
});
canvas.addEventListener("mousemove", (evt) => {
if (dragging && dragIndex !== -1) {
const mousePos = getMousePos(canvas, evt);
studentPositions[dragIndex] = mousePos;
drawStudents();
}
});
canvas.addEventListener("mouseup", () => {
dragging = false;
dragIndex = -1;
});
function getMousePos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top,
};
}
function isInsideCircle(x, y, circleX, circleY, radius) {
const dx = x - circleX;
const dy = y - circleY;
return dx * dx + dy * dy <= radius * radius;
}
3.2 터치 이벤트 처리 (모바일 지원)
canvas.addEventListener("touchstart", handleTouchStart);
canvas.addEventListener("touchmove", handleTouchMove);
canvas.addEventListener("touchend", handleTouchEnd);
function handleTouchStart(evt) {
evt.preventDefault();
const touch = evt.touches[0];
const mousePos = getMousePos(canvas, touch);
studentPositions.forEach((pos, index) => {
if (isInsideCircle(mousePos.x, mousePos.y, pos.x, pos.y, 10)) {
dragging = true;
dragIndex = index;
}
});
}
function handleTouchMove(evt) {
evt.preventDefault();
if (dragging && dragIndex !== -1) {
const touch = evt.touches[0];
const mousePos = getMousePos(canvas, touch);
studentPositions[dragIndex] = mousePos;
drawStudents();
}
}
function handleTouchEnd(evt) {
evt.preventDefault();
dragging = false;
dragIndex = -1;
}
4. 데이터 저장 구현
4.1 설문 데이터 구조화
document.getElementById("submitSurvey").addEventListener("click", async () => {
const surveyResults = {
userName: userName,
studentPositions: students.map((student, index) => ({
name: student,
position: {
x: studentPositions[index].x,
y: studentPositions[index].y,
},
})),
praise: {
name: document.getElementById("praise-name").value,
reason: document.getElementById("praise-reason").value,
},
difficult: {
name: document.getElementById("difficult-name").value,
reason: document.getElementById("difficult-reason").value,
},
otherclass: {
friendlyName: document.getElementById("otherclass-friendly-name").value,
friendlyReason: document.getElementById("good-friend-reason").value,
badName: document.getElementById("otherclass-bad-name").value,
badReason: document.getElementById("bad-friend-reason").value,
},
concern: document.getElementById("concern-reason").value,
teacherMessage: document.getElementById("teacher-message").value,
};
try {
await submitSurveyData(surveyResults);
showCompletionPage();
} catch (error) {
console.error("제출 중 오류 발생:", error);
alert("설문 제출 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
5. 사용자 경험 개선
5.1 단계별 진행
const prevButton = document.getElementById("prevButton");
const nextButton = document.getElementById("nextButton");
nextButton.addEventListener("click", () => {
// 현재 학생 위치 저장
const currentPositions = studentPositions.map((pos) => ({
x: pos.x,
y: pos.y,
}));
// 설문조사 폼 표시
surveyContainer.style.display = "block";
nextButton.disabled = true;
});
5.2 완료 페이지 표시
function showCompletionPage() {
const completePage = document.createElement("div");
completePage.innerHTML = `
<h1>설문이 제출되었습니다.</h1>
<p>제출해 주셔서 감사합니다.</p>
`;
document.body.innerHTML = "";
document.body.appendChild(completePage);
}
6. 에러 처리
6.1 입력 검증
function validateSurveyData(data) {
if (!data.userName) {
throw new Error("이름을 입력해주세요.");
}
if (data.studentPositions.length === 0) {
throw new Error("학생 위치 데이터가 없습니다.");
}
// 필수 입력 항목 검증
if (!data.praise.name || !data.praise.reason) {
throw new Error("칭찬하고 싶은 친구와 이유를 모두 입력해주세요.");
}
}
마치며
Canvas API를 활용한 인터랙티브 관계도 구현은 학생들이 직관적으로 자신의 교우관계를 표현할 수 있게 해줍니다. 또한 터치 이벤트 지원을 통해 모바일 환경에서도 원활한 사용이 가능합니다. 추가 설문 항목들과 함께 종합적인 교우관계 데이터를 수집할 수 있는 기반을 마련했습니다.