尚未检测到更新。GitHub 发布版本后会显示在这里。
正在加载完整 README。 以下为已保存的摘录。
QR로 접속해 직위·성명·전자서명을 남기면 구글 스프레드시트에 인쇄용 등록부가 자동으로 쌓이는 Google Apps Script 웹앱. 전주솔내고등학교에서 쓰고 있고, 다른 학교도 사본을 떠서 쓸 수 있게 정리한 저장소다. 설치 안내서는 이 저장소에 없다. 스프레드시트 안의 📖 설치 안내 시트가 안내서이며, 내용은 Code.gs의 guideContent_()에 있다. 문구를 고치려면 그 함수를 고치고 설치안내시트만들기를 다시 실행한다. 템플릿 스프레드시트 사본 뜨기. 스크립트가 스프레드시트에 바인딩되어 있어서, 시트를 복사하면 코드가 함께 복사된다. • 템플릿: [템플릿] 통합연수등록부(다른 학교 공유용) • 사본 뜨기 링크: https://docs.google.com/spreadsheets/d/1eVJs9K9W5wlKVI7V4-db2pwyGH-Lu8oedTQSuB-11U8/copy (누르면 바로 "사본 만들기" 화면이 뜬다. 템플릿을 새로 관리해야 하면 그 시트의 .../edit 주소를 .../copy 로 바꾸면 된다) • 공유 설정: 링크가 있는 모든 사용자 · 뷰어 (+ 뷰어에게 복사 허용)查看完整 README ↗
QR로 접속해 직위·성명·전자서명을 남기면 구글 스프레드시트에 인쇄용 등록부가 자동으로 쌓이는 Google Apps Script 웹앱. 전주솔내고등학교에서 쓰고 있고, 다른 학교도 사본을 떠서 쓸 수 있게 정리한 저장소다.
설치 안내서는 이 저장소에 없다. 스프레드시트 안의
📖 설치 안내시트가 안내서이며, 내용은Code.gs의guideContent_()에 있다. 문구를 고치려면 그 함수를 고치고설치안내시트만들기를 다시 실행한다.
템플릿 스프레드시트 사본 뜨기. 스크립트가 스프레드시트에 바인딩되어 있어서, 시트를 복사하면 코드가 함께 복사된다.
[템플릿] 통합연수등록부(다른 학교 공유용).../edit 주소를 .../copy 로 바꾸면 된다)받는 쪽은 /copy 링크 하나만 누르면 시트·코드·안내서가 한 번에 온다. 코드를 따로 받거나 파일을 만들 필요가 없다.
스크립트 속성은 복사되지 않는다(확인함). 그래서 학교마다 직접 설정한다.
| 속성 | 용도 | 없으면 |
|---|---|---|
ADMIN_PASSWORD | 담당자 화면 비밀번호 | 담당자 메뉴가 열리지 않음 (의도된 동작) |
SCHOOL_EMBLEM | 교표 (data: 또는 https: URL) | 학교명만 표시. 나머지는 정상 동작 |
배포도 딸려가지 않는다. 각 학교가 직접 배포해야 웹앱 URL이 생긴다. 템플릿 자체는 배포할 필요가 없다.
| 파일 | 역할 |
|---|---|
Code.gs | 서버 로직 · 시트 서식 · 설치 안내 시트 생성 |
Register.html | 참석자 등록 화면 (서명 캔버스) |
Admin.html | 담당자 화면 (링크 발급 · 명단 · 인쇄) |
교표는 파일이 아니라 스크립트 속성에 있다. (예전엔 Logo.html이 있었으나 제거)
| 함수 | 언제 |
|---|---|
설치안내시트만들기 | 템플릿을 만들 때 한 번. 안내 문구를 고친 뒤에도 재실행 |
교표설정하기 | 드라이브에 이름이 교표인 이미지를 올려두고 실행 |
교표지우기 | 교표를 뺄 때 |
SpreadsheetApp.getUi().alert()를 편집기 실행 함수에 넣지 말 것. 알림창은 스프레드시트 탭에 뜨는데 편집기에서 실행하면 그 탭을 보고 있지 않으므로, 아무도 누르지 않는 [확인]을 기다리다 시간 초과로 죽는다. try/catch로도 못 잡는다(예외가 아니라 무한 대기). 결과는 Logger.log로 알린다.
줄마다 getRange().setXxx()를 부르지 말 것. setter 하나가 서버 왕복 한 번이다. 74줄 × 5개 = 370회면 실행이 10분을 넘긴다. 열 전체 배열을 만들어 setFontSizes/setBackgrounds처럼 한 번에 넣고, 테두리는 getRangeList로 묶는다. (buildGuideSheet_ 참고)
시트를 다시 만들 때는 새로 만든 뒤에 옛것을 지울 것. 반대로 하면 그 시트가 문서의 유일한 시트일 때 '문서의 모든 시트를 삭제할 수 없습니다'로 막힌다. 연수 시트가 아직 없는 새 사본이 정확히 그 상태다. (설치안내시트만들기 참고)
안내 시트는 연수가 아니다. listEventSheets·getAttendees·makePrintSheet가 시트를 훑을 때 📖 설치 안내를 걸러야 한다. eventSheets_() / firstEventSheet_()를 쓸 것. 안 그러면 연수 드롭다운에 안내 시트가 뜨고, 연수 미선택 시 안내 시트를 읽는다.
include()는 템플릿을 평가하지 않는다. createHtmlOutputFromFile은 원본을 그대로 반환하므로 포함되는 파일 안에서는 <? ?>가 동작하지 않는다.
이 저장소는 배포 템플릿을 공개하기 위해 새로 시작했다. 아래는 처음 운영하던 코드에서 공개 전에 고친 것들이며, 그 이전 이력은 이 저장소에 없다.
저장형 XSS — signature에 data:image/png;base64,AAA" onerror="..." 를 보내면 서버가 그대로 시트에 저장하고, 담당자가 명단을 조회할 때 <img> 속성을 탈출해 실행됐다. 그 화면엔 ADMIN_PW 변수에 실제 비밀번호가 들어 있어 유출로 이어진다. 등록 링크만 있으면 누구나 가능했다. 서버에서 isSignatureDataUrl_()로 형식을 강제하고(핵심), 출력 시에도 이스케이프한다. 기존 indexOf('data:image')===0 검사는 '시작'만 봐서 뒤에 붙는 따옴표를 막지 못했다.
클라이언트가 준 sheetId 신뢰 — 웹앱이 '실행: 나'로 배포되므로, 등록 링크를 가진 사람이 임의의 시트 ID를 보내면 소유자 권한으로 남의 시트에 쓸 수 있었다. 추적해보니 어떤 화면도 sheetId를 보내지 않는 죽은 경로였기에 getActiveSpreadsheet() 전용으로 바꿔 기능 손실 없이 제거했다.
비밀번호가 코드에 — 코드를 나눠주면 비밀번호도 퍼진다. PropertiesService로 옮기고, 네 함수에 중복돼 있던 검사를 requireAdmin_() 하나로 합쳤다.
위 세 가지는 이 저장소의 코드에 모두 반영되어 있다. 사본을 뜨면 고쳐진 상태로 시작한다.
통합연수등록부 © 2026 KIMYERANG.
학교에서 자유롭게 쓰실 수 있습니다. 만든 이의 허락 없이
재배포하거나 자기 저작물로 공개하는 것은 금지하며,
이 저작권 표시를 지우지 말아 주세요.
경계선은 **'고쳤느냐'가 아니라 '밖으로 내보냈느냐'**다.
해도 되는 것 — 학교에서 쓰는 데 필요한 일은 전부
허락이 필요한 것 — 밖으로 내보내는 일
저작권 표시는 앱 화면 안에 있어서 사본을 뜨면 함께 따라간다. 지우지만 않으면 된다.
쓰다가 좋아졌으면 알려주면 반갑고, 문제가 있으면 이슈로 남겨주면 된다.