/** @OnlyCurrentDoc */ // 9차시 루트 C 강사 예시 — 반짝 교실판(전자칠판 대시보드)의 하루 기록을 이 시트에 남깁니다. // 남기는 것: 체크판(날짜 · 항목 · 내용 · 완료 수 · 아직 번호 · 결석 · 뺀 번호), 출결(결석 · 지각 · 조퇴), 기분 날씨 개수, 알림장 · 전달사항. // 이름은 받지 않고 번호만. 출결은 NEIS 입력 전 보조 메모용입니다. // 같은 날 다시 저장하면 새 줄을 더하지 않고 그날 줄을 고칩니다. 다음 날이 되면 화면은 비워지고 시트에 새 줄이 생깁니다. // 화면 설정(시간표 · 교시 시각 · 학생 수 등)은 스크립트 속성에 저장합니다. // 이 시트만 다루고(@OnlyCurrentDoc) 밖으로 보내지 않습니다. // // 배포: 확장 프로그램 > Apps Script > 파일 두 개(Code.gs, index) 붙여 넣기 // > 배포 > 새 배포 > 웹 앱 > 실행: 나 · 액세스: 나만 > 배포 > 웹 앱 주소를 전자칠판 브라우저에서 열기(교사 계정 로그인) // index 파일은 고치지 않아도 됩니다. 이 파일도 고치지 않아도 됩니다. const TABS = { check: { name: '체크 기록', head: ['날짜', '요일', '판', '항목', '내용', '완료', '대상', '아직 번호', '결석', '뺀 번호', '고친 시각'], text: [1, 8, 9, 10] }, att: { name: '출결', head: ['날짜', '요일', '결석', '지각', '조퇴', '출석', '메모', '고친 시각'], text: [1, 3, 4, 5] }, mood: { name: '기분 날씨', head: ['날짜', '요일', '최고', '평온', '졸려요', '속상해요', '모두', '고친 시각'], text: [1] }, note: { name: '알림장', head: ['날짜', '요일', '알림장', '전달사항', '고친 시각'], text: [1] } }; function doGet() { return HtmlService.createHtmlOutputFromFile('index') .setTitle('반짝 교실판') .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } function loadState() { return PropertiesService.getScriptProperties().getProperty('state') || ''; } function saveState(json, rec) { if (typeof json !== 'string' || json.length > 9000) throw new Error('저장할 내용이 너무 길어요. 알림장을 줄여 주세요.'); JSON.parse(json); // 형식이 맞는지만 확인 const lock = LockService.getScriptLock(); lock.waitLock(10000); // 전자칠판과 교사 노트북에서 동시에 눌러도 줄이 겹치지 않게 try { PropertiesService.getScriptProperties().setProperty('state', json); if (rec && /^\d{4}-\d{2}-\d{2}$/.test(rec.day)) writeDay_(rec); } finally { lock.releaseLock(); } return true; } function writeDay_(rec) { const now = new Date(); (rec.boards || []).slice(0, 3).forEach(b => { upsert_('check', [rec.day, clip_(rec.yo, 1), Number(b[0]) || 0, clip_(b[1], 12), clip_(b[2], 40), Number(b[3]) || 0, Number(b[4]) || 0, clip_(b[5], 200), clip_(b[6], 200), clip_(b[7], 200), now], true); }); const a = rec.att || {}; upsert_('att', [rec.day, clip_(rec.yo, 1), clip_(a.abs, 200), clip_(a.late, 200), clip_(a.early, 200), Number(a.here) || 0, clip_(a.memo, 100), now], false); const m = (rec.mood || []).slice(0, 4).map(x => Number(x) || 0); while (m.length < 4) m.push(0); upsert_('mood', [rec.day, clip_(rec.yo, 1), ...m, m.reduce((a, b) => a + b, 0), now], false); if (String(rec.notice || '').trim() || String(rec.msgs || '').trim()) upsert_('note', [rec.day, clip_(rec.yo, 1), clip_(rec.notice, 1000), clip_(rec.msgs, 1000), now], false); } // 날짜(체크 기록은 날짜 + 판)가 같은 줄이 있으면 고치고, 없으면 아래에 더합니다. function upsert_(key, row, byBoard) { const t = TABS[key]; const ss = SpreadsheetApp.getActiveSpreadsheet(); let sh = ss.getSheetByName(t.name); if (!sh) { sh = ss.insertSheet(t.name); sh.getRange(1, 1, 1, t.head.length).setValues([t.head]).setFontWeight('bold'); sh.setFrozenRows(1); } const last = sh.getLastRow(); let r = 0; if (last > 1) { const from = Math.max(2, last - 59); // 최근 60줄에서만 찾기 const keys = sh.getRange(from, 1, last - from + 1, 3).getDisplayValues(); for (let i = keys.length - 1; i >= 0; i--) { if (keys[i][0] === row[0] && (!byBoard || keys[i][2] === String(row[2]))) { r = from + i; break; } } } if (!r) r = last + 1; t.text.forEach(c => sh.getRange(r, c).setNumberFormat('@')); // 날짜와 번호 목록이 숫자·날짜로 바뀌지 않게 sh.getRange(r, 1, 1, row.length).setValues([row]); } function clip_(v, n) { return String(v == null ? '' : v).slice(0, n); }