You cannot see this page without javascript.

<!DOCTYPE html>

<html lang="ko">

<head>

  <meta charset="UTF-8">

  <title>출석부 앱</title>

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <style>

    * {

      box-sizing: border-box;

      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

    }

 

    body {

      margin: 0;

      padding: 16px;

      background: #f3f4f6;

      display: flex;

      justify-content: center;

    }

 

    .container {

      width: 100%;

      max-width: 960px;

      background: #ffffff;

      border-radius: 16px;

      box-shadow: 0 10px 30px rgba(0,0,0,0.08);

      padding: 20px 24px 28px;

    }

 

    h1 {

      margin: 0 0 4px;

      font-size: 24px;

      font-weight: 700;

    }

 

    .subtitle {

      color: #6b7280;

      font-size: 13px;

      margin-bottom: 16px;

    }

 

    .row {

      display: flex;

      flex-wrap: wrap;

      gap: 12px;

      margin-bottom: 12px;

    }

 

    .field {

      flex: 1 1 180px;

      min-width: 160px;

    }

 

    label {

      font-size: 12px;

      font-weight: 600;

      color: #374151;

      display: block;

      margin-bottom: 4px;

    }

 

    input[type="text"],

    input[type="date"],

    textarea {

      width: 100%;

      padding: 8px 10px;

      border-radius: 8px;

      border: 1px solid #d1d5db;

      font-size: 13px;

      outline: none;

      transition: border-color 0.15s, box-shadow 0.15s;

      background: #f9fafb;

    }

 

    input[type="text"]:focus,

    input[type="date"]:focus,

    textarea:focus {

      border-color: #3b82f6;

      box-shadow: 0 0 0 2px rgba(59,130,246,0.15);

      background: #ffffff;

    }

 

    textarea {

      resize: vertical;

      min-height: 80px;

    }

 

    .btn {

      display: inline-flex;

      align-items: center;

      justify-content: center;

      gap: 6px;

      padding: 8px 14px;

      border-radius: 999px;

      border: none;

      background: #3b82f6;

      color: white;

      font-size: 13px;

      font-weight: 600;

      cursor: pointer;

      transition: transform 0.05s ease, box-shadow 0.1s ease, background 0.15s;

      white-space: nowrap;

    }

 

    .btn:hover {

      background: #2563eb;

      box-shadow: 0 6px 16px rgba(37,99,235,0.35);

      transform: translateY(-1px);

    }

 

    .btn:active {

      transform: translateY(0);

      box-shadow: none;

    }

 

    .btn.outline {

      background: white;

      color: #111827;

      border: 1px solid #d1d5db;

      box-shadow: none;

    }

 

    .btn.outline:hover {

      background: #f3f4f6;

    }

 

    .btn.warn {

      background: #ef4444;

    }

 

    .btn.warn:hover {

      background: #dc2626;

      box-shadow: 0 6px 16px rgba(220,38,38,0.35);

    }

 

    .btn-group {

      display: flex;

      flex-wrap: wrap;

      gap: 8px;

      margin: 8px 0 4px;

    }

 

    table {

      width: 100%;

      border-collapse: collapse;

      margin-top: 12px;

      font-size: 13px;

      border-radius: 12px;

      overflow: hidden;

    }

 

    thead {

      background: #f3f4f6;

    }

 

    th, td {

      padding: 8px 10px;

      border-bottom: 1px solid #e5e7eb;

      text-align: left;

    }

 

    th {

      font-weight: 600;

      font-size: 12px;

      color: #4b5563;

    }

 

    tbody tr:nth-child(even) {

      background: #fafafa;

    }

 

    tbody tr:hover {

      background: #eff6ff;

    }

 

    .center {

      text-align: center;

    }

 

    .status-group {

      display: flex;

      justify-content: center;

      gap: 6px;

      font-size: 11px;

    }

 

    .status-group label {

      font-weight: 400;

      color: #374151;

    }

 

    .status-group input {

      margin-right: 2px;

    }

 

    .summary {

      display: flex;

      justify-content: flex-end;

      gap: 12px;

      margin-top: 8px;

      font-size: 12px;

      color: #374151;

    }

 

    .summary span.badge {

      display: inline-flex;

      align-items: center;

      gap: 4px;

      padding: 4px 8px;

      border-radius: 999px;

      background: #f3f4f6;

      font-weight: 500;

    }

 

    .summary span.badge strong {

      font-weight: 700;

    }

 

    .footer-note {

      margin-top: 10px;

      font-size: 11px;

      color: #9ca3af;

      text-align: right;

    }

 

    @media (max-width: 640px) {

      .container {

        padding: 16px;

      }

 

      table {

        font-size: 12px;

      }

 

      th, td {

        padding: 6px;

      }

 

      .status-group {

        flex-direction: column;

        align-items: flex-start;

      }

    }

  </style>

</head>

<body>

  <div class="container">

    <h1>출석부</h1>

    <div class="subtitle">반 이름과 날짜를 설정하고 학생 출석을 기록하세요. (브라우저에 자동 저장)</div>

 

    <!-- 반/날짜 설정 -->

    <div class="row">

      <div class="field">

        <label for="className">반 이름</label>

        <input type="text" id="className" placeholder="예) 3학년 2반">

      </div>

      <div class="field" style="max-width: 200px;">

        <label for="date">날짜</label>

        <input type="date" id="date">

      </div>

      <div class="field" style="flex: 0 0 auto; align-self: flex-end;">

        <button class="btn outline" id="loadBtn">저장된 출석 불러오기</button>

      </div>

    </div>

 

    <!-- 학생 목록 입력 -->

    <div class="row">

      <div class="field" style="flex: 2 1 280px;">

        <label for="namesInput">학생 이름 (한 줄에 한 명씩)</label>

        <textarea id="namesInput" placeholder="예)&#10;홍길동&#10;이몽룡&#10;성춘향"></textarea>

      </div>

      <div class="field" style="flex: 1 1 180px;">

        <label>학생 목록 적용</label>

        <div class="btn-group">

          <button class="btn" id="applyNamesBtn">학생목록 적용</button>

          <button class="btn outline" id="addOneBtn">한 명 추가</button>

        </div>

        <input type="text" id="singleNameInput" placeholder="추가할 학생 이름">

      </div>

    </div>

 

    <!-- 출석 테이블 -->

    <table id="attendanceTable">

      <thead>

        <tr>

          <th class="center" style="width: 50px;">번호</th>

          <th>이름</th>

          <th class="center" style="width: 200px;">출석 상태</th>

          <th>비고</th>

        </tr>

      </thead>

      <tbody>

        <!-- JS로 생성 -->

      </tbody>

    </table>

 

    <!-- 요약/버튼 -->

    <div class="summary">

      <span class="badge">출석 <strong id="presentCount">0</strong>명</span>

      <span class="badge">지각 <strong id="lateCount">0</strong>명</span>

      <span class="badge">결석 <strong id="absentCount">0</strong>명</span>

      <span class="badge">총원 <strong id="totalCount">0</strong>명</span>

    </div>

 

    <div class="btn-group" style="justify-content: flex-end; margin-top: 10px;">

      <button class="btn" id="saveBtn">오늘 출석 저장</button>

      <button class="btn outline" id="exportBtn">CSV로 내보내기</button>

      <button class="btn warn outline" id="resetBtn">새로 시작</button>

    </div>

 

    <div class="footer-note">

      저장된 데이터는 이 브라우저(이 컴퓨터)에만 보관됩니다. (localStorage 사용)

    </div>

  </div>

 

  <script>

    // 엘리먼트 참조

    const classNameInput = document.getElementById('className');

    const dateInput = document.getElementById('date');

    const namesInput = document.getElementById('namesInput');

    const singleNameInput = document.getElementById('singleNameInput');

    const applyNamesBtn = document.getElementById('applyNamesBtn');

    const addOneBtn = document.getElementById('addOneBtn');

    const attendanceTableBody = document.querySelector('#attendanceTable tbody');

    const presentCountEl = document.getElementById('presentCount');

    const lateCountEl = document.getElementById('lateCount');

    const absentCountEl = document.getElementById('absentCount');

    const totalCountEl = document.getElementById('totalCount');

    const saveBtn = document.getElementById('saveBtn');

    const loadBtn = document.getElementById('loadBtn');

    const exportBtn = document.getElementById('exportBtn');

    const resetBtn = document.getElementById('resetBtn');

 

    // 오늘 날짜 기본값 세팅

    function setToday() {

      const today = new Date();

      const y = today.getFullYear();

      const m = String(today.getMonth() + 1).padStart(2, '0');

      const d = String(today.getDate()).padStart(2, '0');

      dateInput.value = `${y}-${m}-${d}`;

    }

 

    setToday();

 

    // 학생 목록으로 테이블 생성

    function buildTableFromNames(names) {

      attendanceTableBody.innerHTML = '';

      names.forEach((name, idx) => {

        const trimmed = name.trim();

        if (!trimmed) return;

 

        const tr = document.createElement('tr');

 

        // 번호

        const tdNo = document.createElement('td');

        tdNo.className = 'center';

        tdNo.textContent = idx + 1;

        tr.appendChild(tdNo);

 

        // 이름

        const tdName = document.createElement('td');

        tdName.textContent = trimmed;

        tr.appendChild(tdName);

 

        // 출석 상태

        const tdStatus = document.createElement('td');

        tdStatus.className = 'center';

        const group = document.createElement('div');

        group.className = 'status-group';

 

        const statuses = [

          { value: 'present', label: '출석' },

          { value: 'late', label: '지각' },

          { value: 'absent', label: '결석' }

        ];

 

        const groupName = `status-${idx}`;

 

        statuses.forEach(st => {

          const label = document.createElement('label');

          const radio = document.createElement('input');

          radio.type = 'radio';

          radio.name = groupName;

          radio.value = st.value;

          if (st.value === 'present') radio.checked = true;

          radio.addEventListener('change', updateSummary);

          label.appendChild(radio);

          label.appendChild(document.createTextNode(st.label));

          group.appendChild(label);

        });

 

        tdStatus.appendChild(group);

        tr.appendChild(tdStatus);

 

        // 비고

        const tdRemark = document.createElement('td');

        const remarkInput = document.createElement('input');

        remarkInput.type = 'text';

        remarkInput.placeholder = '메모';

        remarkInput.style.width = '100%';

        remarkInput.style.padding = '4px 6px';

        remarkInput.style.borderRadius = '6px';

        remarkInput.style.border = '1px solid #e5e7eb';

        remarkInput.style.fontSize = '12px';

        tdRemark.appendChild(remarkInput);

        tr.appendChild(tdRemark);

 

        attendanceTableBody.appendChild(tr);

      });

 

      updateSummary();

    }

 

    // 요약 카운트 업데이트

    function updateSummary() {

      const rows = attendanceTableBody.querySelectorAll('tr');

      let present = 0, late = 0, absent = 0, total = 0;

 

      rows.forEach(row => {

        total++;

        const radios = row.querySelectorAll('input[type="radio"]');

        radios.forEach(r => {

          if (r.checked) {

            if (r.value === 'present') present++;

            else if (r.value === 'late') late++;

            else if (r.value === 'absent') absent++;

          }

        });

      });

 

      presentCountEl.textContent = present;

      lateCountEl.textContent = late;

      absentCountEl.textContent = absent;

      totalCountEl.textContent = total;

    }

 

    // localStorage 키 생성

    function getStorageKey() {

      const cls = classNameInput.value.trim();

      const date = dateInput.value;

      if (!cls || !date) return null;

      return `attendance_${cls}_${date}`;

    }

 

    // 저장 데이터 만들기

    function collectData() {

      const cls = classNameInput.value.trim();

      const date = dateInput.value;

      if (!cls) {

        alert('반 이름을 입력해주세요.');

        return null;

      }

      if (!date) {

        alert('날짜를 선택해주세요.');

        return null;

      }

 

      const rows = attendanceTableBody.querySelectorAll('tr');

      if (rows.length === 0) {

        alert('학생 목록이 비어 있습니다. 학생을 먼저 추가해주세요.');

        return null;

      }

 

      const students = [];

      rows.forEach(row => {

        const name = row.children[1].textContent.trim();

        const radios = row.querySelectorAll('input[type="radio"]');

        let status = 'present';

        radios.forEach(r => {

          if (r.checked) status = r.value;

        });

        const remark = row.children[3].querySelector('input').value.trim();

        students.push({ name, status, remark });

      });

 

      return {

        className: cls,

        date,

        students

      };

    }

 

    // 저장

    saveBtn.addEventListener('click', () => {

      const data = collectData();

      if (!data) return;

 

      const key = getStorageKey();

      if (!key) return;

 

      localStorage.setItem(key, JSON.stringify(data));

      alert('출석 데이터가 저장되었습니다.\n(이 브라우저에서만 보관됩니다)');

    });

 

    // 불러오기

    loadBtn.addEventListener('click', () => {

      const key = getStorageKey();

      if (!key) {

        alert('반 이름과 날짜를 모두 입력/선택한 후 불러올 수 있습니다.');

        return;

      }

 

      const raw = localStorage.getItem(key);

      if (!raw) {

        alert('해당 반/날짜로 저장된 출석 데이터가 없습니다.');

        return;

      }

 

      try {

        const data = JSON.parse(raw);

        if (!data.students || !Array.isArray(data.students)) throw new Error();

        // 테이블 재구성

        attendanceTableBody.innerHTML = '';

        data.students.forEach((st, idx) => {

          const tr = document.createElement('tr');

 

          const tdNo = document.createElement('td');

          tdNo.className = 'center';

          tdNo.textContent = idx + 1;

          tr.appendChild(tdNo);

 

          const tdName = document.createElement('td');

          tdName.textContent = st.name;

          tr.appendChild(tdName);

 

          const tdStatus = document.createElement('td');

          tdStatus.className = 'center';

          const group = document.createElement('div');

          group.className = 'status-group';

          const statuses = [

            { value: 'present', label: '출석' },

            { value: 'late', label: '지각' },

            { value: 'absent', label: '결석' }

          ];

          const groupName = `status-${idx}`;

          statuses.forEach(s => {

            const label = document.createElement('label');

            const radio = document.createElement('input');

            radio.type = 'radio';

            radio.name = groupName;

            radio.value = s.value;

            if (st.status === s.value) radio.checked = true;

            radio.addEventListener('change', updateSummary);

            label.appendChild(radio);

            label.appendChild(document.createTextNode(s.label));

            group.appendChild(label);

          });

          tdStatus.appendChild(group);

          tr.appendChild(tdStatus);

 

          const tdRemark = document.createElement('td');

          const remarkInput = document.createElement('input');

          remarkInput.type = 'text';

          remarkInput.value = st.remark || '';

          remarkInput.placeholder = '메모';

          remarkInput.style.width = '100%';

          remarkInput.style.padding = '4px 6px';

          remarkInput.style.borderRadius = '6px';

          remarkInput.style.border = '1px solid #e5e7eb';

          remarkInput.style.fontSize = '12px';

          tdRemark.appendChild(remarkInput);

          tr.appendChild(tdRemark);

 

          attendanceTableBody.appendChild(tr);

        });

 

        updateSummary();

        alert('저장된 출석 데이터를 불러왔습니다.');

      } catch (e) {

        console.error(e);

        alert('저장된 데이터 형식에 문제가 있습니다.');

      }

    });

 

    // CSV로 내보내기

    exportBtn.addEventListener('click', () => {

      const data = collectData();

      if (!data) return;

 

      const header = ['번호', '이름', '상태', '비고'];

      const rows = data.students.map((st, idx) => {

        let statusText = '';

        if (st.status === 'present') statusText = '출석';

        else if (st.status === 'late') statusText = '지각';

        else if (st.status === 'absent') statusText = '결석';

 

        // CSV 특수문자 처리 (큰따옴표 감싸기)

        const escape = (value) => {

          if (value == null) return '';

          const v = String(value).replace(/"/g, '""');

          return `"${v}"`;

        };

 

        return [

          idx + 1,

          escape(st.name),

          escape(statusText),

          escape(st.remark || '')

        ].join(',');

      });

 

      const csvContent = [

        header.join(','),

        ...rows

      ].join('\r\n');

 

      const blob = new Blob(

        ['\ufeff' + csvContent],

        { type: 'text/csv;charset=utf-8;' }

      );

 

      const cls = data.className.replace(/\s+/g, '_');

      const date = data.date;

      const fileName = `출석부_${cls}_${date}.csv`;

 

      const link = document.createElement('a');

      const url = URL.createObjectURL(blob);

      link.href = url;

      link.download = fileName;

      document.body.appendChild(link);

      link.click();

      document.body.removeChild(link);

      URL.revokeObjectURL(url);

    });

 

    // 새로 시작

    resetBtn.addEventListener('click', () => {

      if (!confirm('현재 화면의 출석 내용을 모두 지우시겠습니까? (저장된 데이터는 남아 있습니다)')) return;

      attendanceTableBody.innerHTML = '';

      namesInput.value = '';

      singleNameInput.value = '';

      updateSummary();

    });

 

    // 학생목록 적용 버튼

    applyNamesBtn.addEventListener('click', () => {

      const text = namesInput.value;

      if (!text.trim()) {

        alert('학생 이름을 먼저 입력해주세요. (한 줄에 한 명씩)');

        return;

      }

      const names = text.split('\n');

      buildTableFromNames(names);

    });

 

    // 한 명 추가 버튼

    addOneBtn.addEventListener('click', () => {

      const name = singleNameInput.value.trim();

      if (!name) {

        alert('추가할 학생 이름을 입력해주세요.');

        return;

      }

      const currentNames = [];

      attendanceTableBody.querySelectorAll('tr').forEach(row => {

        currentNames.push(row.children[1].textContent.trim());

      });

      currentNames.push(name);

      buildTableFromNames(currentNames);

      singleNameInput.value = '';

      namesInput.value = currentNames.join('\n');

    });

  </script>

</body>

</html>

  • profile
    송정석 21 시간 전

    1. 위 코드를 메모장/VSCode 등에 붙여넣고 attendance.html 로 저장
    2. 저장한 파일을 더블클릭해서 브라우저(크롬,엣지 등)로 열기
    3. 상단에 반이름 입력, 날짜 선택(기본은 오늘 날짜)
    4. 왼쪽 큰 칸에 학생 이름을, 한줄에 한 명씩 입력 - 학생목록 적용 클릭