# 온라인 출석부 (QR 서명 방식) ## 구조 구글 계정이 여러 개 있는 폰에서 `script.google.com` 페이지를 직접 열면, 구글이 계정 컨텍스트를 정하는 과정에서 주소가 `.../u/1/exec`, `.../u/3/exec` 처럼 바뀌면서 접속이 안 되는 문제가 있습니다. 이 문제는 링크를 단축 URL로 감싸도, 최종적으로 그 페이지를 "열람"하는 순간 다시 발생합니다. 그래서 이 구성에서는 **학생이 여는 페이지는 구글 도메인이 아닌 곳에 두고**, Apps Script는 그 페이지가 백그라운드에서 `fetch()`로 호출하는 JSON API 역할만 하도록 나눴습니다. 브라우저가 `script.google.com`을 화면에 열람하는 게 아니라 JS가 조용히 API만 호출하기 때문에 계정 선택 문제가 아예 발생하지 않습니다. - `Code.gs` → Google Apps Script에 붙여넣음. 서명 이미지 저장 + 시트 기록을 담당하는 JSON API (`doPost`) - `site/index.html` → **Apps Script가 아닌** 일반 정적 호스팅(GitHub Pages 등)에 올림. 학생이 QR로 여는 실제 페이지 (이름 입력 + 손글씨 서명 캔버스) 기록되는 열: 날짜, 이름, 입실시각, 퇴실시각, 학습시간(자동 계산), 입실서명(이미지), 퇴실서명(이미지), 입실기기ID·입실기기정보·퇴실기기ID·퇴실기기정보(기본적으로 숨김 처리됨) - **학습시간**은 입실시각·퇴실시각이 둘 다 있을 때만 자동 계산되는 수식(`=D-C`)입니다. 시각이 바뀌면 자동으로 다시 계산됩니다. - **이름 입력창 자동완성**: 페이지를 열 때 시트에 이미 기록된 이름 목록을 불러와 입력창에 제안으로 띄워줍니다 (브라우저 기본 자동완성 UI 사용, 새 이름도 자유롭게 입력 가능). - 페이지 상단에 학교 로고 이미지가 표시됩니다. 시트는 **학생 1명 + 날짜 1개 = 1행**으로 관리됩니다. 같은 날 같은 학생이 여러 번 서명해도 행이 늘어나지 않고, 다음 규칙으로 같은 행이 갱신됩니다. - **입실**: 그날 기록된 시각보다 더 빠른 경우에만 갱신 (그날 첫 입실 시각 유지) - **퇴실**: 그날 기록된 시각보다 더 늦은 경우에만 갱신 (그날 마지막 퇴실 시각 유지) - 갱신되지 않는 제출은 서명 이미지도 기록되지 않습니다 (시트에 남는 서명은 항상 실제로 채택된 입실/퇴실 시각과 같은 순간에 찍힌 서명입니다). 이 구조라서 날짜별로 등교/하교 시간을 볼 때 피벗테이블이 단순해집니다 — 이미 한 줄에 그날의 입실/퇴실시각이 정리돼 있으므로, 피벗테이블에서 행에 `날짜`(또는 `이름`), 값에 `입실시각`/`퇴실시각`을 그대로 넣으면 됩니다 (별도 MIN/MAX 집계 설정 불필요). > **열 구조가 바뀔 때마다 기존 `출석부` 시트는 그대로 재사용하면 안 됩니다** (헤더가 이미 있으면 코드가 헤더를 다시 쓰지 않기 때문에 새 열이 추가되지 않습니다). 학습시간 열을 추가하면서 다시 한 번 열 구조가 바뀌었으니, 기존 `출석부` 탭 이름을 `출석부_구버전` 등으로 바꿔 보관해두고 Apps Script를 재배포하면 새 구조의 `출석부` 시트가 새로 만들어집니다. ## 1단계 — Apps Script API 배포 1. [sheets.google.com](https://sheets.google.com)에서 새 스프레드시트 생성 (이름 예: "출석부") 2. 상단 메뉴 **확장 프로그램 → Apps Script** 클릭 3. 기본 생성된 `Code.gs` 내용을 전부 지우고, 이 폴더의 `Code.gs` 내용을 붙여넣기 4. `Ctrl+S`로 저장 5. 오른쪽 위 **배포 → 새 배포** - 유형: **웹 앱** - 실행 계정: **나** - 액세스 권한이 있는 사용자: **전체** - 배포 클릭 6. 처음 배포 시 권한 승인 창이 뜨면 **고급 → (프로젝트명)으로 이동 → 허용** (Sheets/Drive 접근 권한 승인 — 본인 계정으로만 실행되므로 안전합니다) 7. 발급된 웹 앱 URL(`https://script.google.com/macros/s/.../exec`)을 복사 — **이 URL은 학생에게 직접 주지 않습니다.** 2단계에서 페이지 안에만 넣습니다. ## 2단계 — 학생용 페이지 호스팅 (구글이 아닌 곳) `site/index.html`을 열어 맨 위 스크립트의 ```js const EXEC_URL = 'PASTE_YOUR_APPS_SCRIPT_WEB_APP_URL_HERE'; ``` 부분을 1단계에서 복사한 실제 URL로 교체한 뒤, 정적 호스팅에 올립니다. 아무 계정 없이 가장 빨리 해보려면: - **Netlify Drop**: [app.netlify.com/drop](https://app.netlify.com/drop)에 `site` 폴더를 그대로 드래그 앤 드롭 → 즉시 URL 발급 (계정 없이도 되지만, 계속 쓰려면 무료 계정 연결 권장) 계속 쓸 주소를 원하면: - **GitHub Pages**: GitHub 저장소에 `site` 폴더 내용을 올리고 저장소 설정에서 Pages를 켜면 `https://<사용자명>.github.io/<저장소명>/` 형태의 고정 주소가 생김 - **Cloudflare Pages / Vercel**: 같은 방식으로 `site` 폴더 하나만 배포하면 됨 (별도 빌드 설정 불필요, 정적 HTML 파일 1개) ## 3단계 — QR 코드 만들기 호스팅에서 받은 주소(구글 도메인이 아닌 주소)로 QR 코드를 생성해 인쇄 후 교실 입구에 부착하세요. - Chrome 주소창 오른쪽 **공유 아이콘 → "QR 코드 만들기"** 기능을 쓰면 별도 사이트 없이 바로 생성됩니다. - 원하는 QR 생성 사이트/앱을 사용해도 무방합니다. 이제 QR을 찍으면 곧바로 `site/index.html` 페이지가 열리고(구글 계정과 무관), 제출 시에만 뒤에서 조용히 Apps Script API를 호출합니다. ## 대리서명 확인 방법 - **입실기기ID/퇴실기기ID** 열(기본 숨김)이 같은데 **이름**이 다른 경우, 같은 기기(브라우저)에서 여러 사람 이름으로 서명이 이뤄진 것이므로 대리서명 의심 대상입니다. 확인할 때만 열을 펼쳐서(선택 후 우클릭 → 열 표시) 기기ID 기준으로 정렬하거나 조건부 서식으로 중복을 표시하면 됩니다. - **입실기기정보/퇴실기기정보** 열로 기종·브라우저가 같은 서명이 몰려 있는지 함께 확인하세요. - 기기ID는 브라우저의 localStorage에 저장되는 값이라, 시크릿 모드나 브라우저 데이터 삭제 시 매번 새로 생성됩니다. 완벽한 기기 식별은 아니고 보조 지표로만 활용하세요. ## 피벗테이블로 일자별 등교/하교 시간 보기 1. `출석부` 시트에서 데이터 범위를 선택하고 **삽입 → 피벗 테이블** 2. 행: `날짜` (필요하면 `이름`도 추가) 3. 값: `입실시각`, `퇴실시각` — 요약 방식은 "없음"(그대로 표시)이나 "최소"/"최대" 어느 걸 골라도 결과가 같습니다 (한 학생·한 날짜에 이미 값이 하나뿐이기 때문입니다). ## 코드 수정 시 주의 - `Code.gs`를 수정한 뒤에는 **배포 → 배포 관리 → 수정(연필 아이콘) → 버전: 새 버전 → 배포**를 해야 반영됩니다. (같은 URL이 유지되므로 `site/index.html`의 `EXEC_URL`은 그대로 둬도 됩니다.) - `site/index.html`을 수정한 뒤에는 호스팅에 다시 올려야 반영됩니다 (GitHub Pages는 push, Netlify Drop은 다시 드래그). ## 자동 생성되는 것들 첫 제출이 들어오면 자동으로: - 스프레드시트에 `출석부` 시트와 헤더가 생성됩니다. - 내 드라이브에 `출석_서명이미지` 폴더가 생성되고, 서명 PNG 파일들이 여기에 저장됩니다 (시트 셀에는 이 이미지가 바로 삽입되어 보입니다).