교우관계 웹 앱 만들기 - 04.분석 페이지 만들기
교우관계 분석 페이지 구현
1. 분석 페이지 구조
분석 페이지는 학생 선택 UI와 4가지 주요 분석 섹션으로 구성됩니다:
<div class="controls">
<select id="studentSelect">
<option value="">학생을 선택하세요</option>
</select>
<button onclick="analyzeData()">분석하기</button>
</div>
<div id="analysis-section">
<div id="visualization"><!-- 관계도 시각화 --></div>
<div id="distance-analysis"><!-- 거리 기반 분석 --></div>
<div id="cluster-analysis"><!-- 군집 분석 --></div>
<div id="preferences-analysis"><!-- 선호도 분석 --></div>
<div id="overall-analysis"><!-- 종합 분석 --></div>
</div>
2. 학생 선택 UI 구현
2.1 학생 목록 초기화
async function populateStudentSelect() {
try {
const response = await fetch(`${SCRIPT_URL}?action=getAnalysisData`);
const result = await response.json();
const studentSelect = document.getElementById("studentSelect");
studentSelect.innerHTML = '<option value="">학생을 선택하세요</option>';
// 중복 제거 및 정렬된 학생 목록 생성
const students = result.data
.map((row) => {
const text = row[0];
const match = text.match(/^([^,]+)/);
return match ? match[1].trim() : null;
})
.filter((name) => name);
const uniqueStudents = [...new Set(students)].sort();
// 옵션 추가
uniqueStudents.forEach((student) => {
const option = document.createElement("option");
option.value = student;
option.textContent = student;
studentSelect.appendChild(option);
});
} catch (error) {
console.error("학생 목록 로드 에러:", error);
showError("학생 목록을 불러오는 중 오류가 발생했습니다");
}
}
3. 관계도 시각화 구현
3.1 위치 데이터 파싱
function parseStudentData(studentData) {
try {
if (!studentData || !studentData[1] || typeof studentData[1] !== "string") {
throw new Error("유효하지 않은 학생 데이터 형식입니다.");
}
const studentsInfo = studentData[1]
.split("\n")
.map((info) => info.trim())
.filter((info) => info.length > 0)
.map((info) => {
const match = info.match(/(.*?):\s*\((\d+),\s*(\d+)\)/);
if (!match) return null;
return {
name: match[1].trim(),
x: parseInt(match[2]),
y: parseInt(match[3]),
};
})
.filter((item) => item !== null);
return studentsInfo;
} catch (error) {
throw new Error(
`학생 데이터 파싱 중 오류가 발생했습니다: ${error.message}`
);
}
}
3.2 관계도 그리기
function visualizeRelationships(studentData) {
const vizDiv = document.getElementById("visualization");
vizDiv.innerHTML = "";
try {
const width = vizDiv.clientWidth;
const height = 500;
const centerX = width / 2;
const centerY = height / 2;
const svg = d3
.select("#visualization")
.append("svg")
.attr("width", width)
.attr("height", height);
const allStudents = parseStudentData(studentData);
// 스케일 설정
const xScale = d3
.scaleLinear()
.domain([0, 500])
.range([50, width - 50]);
const yScale = d3
.scaleLinear()
.domain([0, 500])
.range([50, height - 50]);
// 연결선 그리기
svg
.selectAll("line")
.data(allStudents)
.enter()
.append("line")
.attr("x1", centerX)
.attr("y1", centerY)
.attr("x2", (d) => xScale(d.x))
.attr("y2", (d) => yScale(d.y))
.attr("stroke", "#ddd")
.attr("stroke-width", 1);
// 중심 노드 (선택된 학생)
const centerNode = svg
.append("g")
.attr("transform", `translate(${centerX}, ${centerY})`);
centerNode.append("circle").attr("r", 30).attr("fill", "#1a237e");
centerNode
.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
.attr("fill", "white")
.text(studentData[0]);
// 다른 학생들 노드
const nodes = svg
.selectAll(".student-node")
.data(allStudents)
.enter()
.append("g")
.attr("class", "student-node")
.attr("transform", (d) => `translate(${xScale(d.x)}, ${yScale(d.y)})`);
nodes.append("circle").attr("r", 25).attr("fill", "#2e7d32");
nodes
.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
.attr("fill", "white")
.text((d) => d.name);
} catch (error) {
console.error("시각화 에러:", error);
vizDiv.innerHTML = '<p class="error">시각화 중 오류가 발생했습니다.</p>';
}
}
4. 분석 기능 구현
4.1 거리 기반 분석
function analyzeDistances(studentData) {
try {
const students = parseStudentData(studentData);
const relationships = students
.map((student) => {
const distance = Math.sqrt(
Math.pow(student.x - 250, 2) + Math.pow(student.y - 250, 2)
);
return {
name: student.name,
intimacy: calculateIntimacy(distance),
};
})
.sort((a, b) => b.intimacy - a.intimacy);
const closeStudents = relationships.slice(0, 3);
const farStudents = relationships.slice(-3).reverse();
return `
<div class="sub-section">
<h4>친밀도가 높은 친구들 (상위 3명)</h4>
<ul>
${closeStudents
.map((d) => `<li>${d.name} (친밀도: ${d.intimacy}%)</li>`)
.join("")}
</ul>
<h4>친밀도가 낮은 친구들 (하위 3명)</h4>
<ul>
${farStudents
.map((d) => `<li>${d.name} (친밀도: ${d.intimacy}%)</li>`)
.join("")}
</ul>
</div>
`;
} catch (error) {
return `<p class="error">거리 기반 분석 중 오류가 발생했습니다: ${error.message}</p>`;
}
}
4.2 군집 분석
function analyzeClusters(studentData) {
try {
const students = parseStudentData(studentData);
const clusters = {
"가까운 거리": [],
"보통 거리": [],
"먼 거리": [],
};
students.forEach((student) => {
const distance = Math.sqrt(
Math.pow(student.x - 250, 2) + Math.pow(student.y - 250, 2)
);
if (distance < 100) {
clusters["가까운 거리"].push(student.name);
} else if (distance < 200) {
clusters["보통 거리"].push(student.name);
} else {
clusters["먼 거리"].push(student.name);
}
});
return `
<div class="card-container">
${Object.entries(clusters)
.map(
([group, students]) => `
<div class="card">
<h4>${group} (${students.length}명)</h4>
<ul>
${students.map((name) => `<li>${name}</li>`).join("")}
</ul>
</div>
`
)
.join("")}
</div>
`;
} catch (error) {
return `<p class="error">군집 분석 중 오류가 발생했습니다: ${error.message}</p>`;
}
}
4.3 선호도 분석
function calculatePreferenceScores(students) {
const scores = {};
students.forEach((student) => {
scores[student.name] = 0;
});
students.forEach((student) => {
students
.filter((other) => other.name !== student.name)
.forEach((other) => {
const distance = Math.sqrt(
Math.pow(student.x - other.x, 2) + Math.pow(student.y - other.y, 2)
);
const score = Math.round(Math.max(0, 100 - distance / 5));
scores[other.name] += score;
});
});
return Object.entries(scores)
.map(([name, score]) => ({
name,
score: Math.round(score / (students.length - 1)),
}))
.sort((a, b) => b.score - a.score);
}
4.4 종합 분석
function generateOverallAnalysis(parsedStudents) {
const totalStudents = parsedStudents.length;
const closeCount = parsedStudents.filter(
(student) =>
Math.sqrt(Math.pow(student.x - 250, 2) + Math.pow(student.y - 250, 2)) <
100
).length;
const intimacyRate = Math.round((closeCount / totalStudents) * 100);
return `
<div class="sub-section">
<div class="summary-point">
<h4>전체 친구 관계 요약</h4>
<p>전체 친구 수: ${totalStudents}명</p>
<p>친밀한 관계의 친구 비율: ${intimacyRate}%</p>
</div>
<div class="summary-point">
<h4>관계 패턴 분석</h4>
<p>${getRelationshipPattern(intimacyRate)}</p>
</div>
</div>
`;
}
5. 분석 결과 통합
async function analyzeData() {
const studentSelect = document.getElementById("studentSelect");
const selectedStudent = studentSelect.value;
if (!selectedStudent) {
alert("학생을 선택해주세요.");
return;
}
try {
const response = await fetch(`${SCRIPT_URL}?action=getAnalysisData`);
const result = await response.json();
const studentData = result.data.find((row) => row[0] === selectedStudent);
if (!studentData) {
throw new Error("선택된 학생의 데이터를 찾을 수 없습니다.");
}
const parsedStudents = parseStudentData(studentData);
// 분석 실행
visualizeRelationships(studentData);
document.getElementById("distance-analysis").innerHTML =
analyzeDistances(studentData);
document.getElementById("cluster-analysis").innerHTML =
analyzeClusters(studentData);
document.getElementById("preferences-analysis").innerHTML =
analyzeRelationships(studentData);
document.getElementById("overall-analysis").innerHTML =
generateOverallAnalysis(parsedStudents);
} catch (error) {
console.error("분석 에러:", error);
alert(`분석 중 오류가 발생했습니다: ${error.message}`);
}
}
마치며
분석 페이지는 학생들의 교우관계 데이터를 다각도로 분석하여 시각적으로 표현합니다. D3.js를 활용한 관계도 시각화와 함께 거리 기반, 군집, 선호도, 종합 분석을 통해 학급 내 교우관계를 종합적으로 파악할 수 있도록 구현했습니다. 각 분석 결과는 직관적인 UI로 표현되어 교사가 쉽게 이해하고 활용할 수 있습니다.