교우관계 분석 페이지 구현

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로 표현되어 교사가 쉽게 이해하고 활용할 수 있습니다.