Search
Duplicate

구글 Appa Script 활용 개별점수확인

목차(클릭하세요)
오류가 주로 나는 부분은 다음 2가지 포인트 1. 배포용으로 만드는 과정에서 설정 2. 데이터베이스 역할을 하는 구글 스프레드시트의 탭이름
교실혁신 선도교사 연수를 진행하며, 각 학교에서 학생들의 지필 또는 수행평가를 조회하는데 있어, 자신의 점수가 아닌 타인의 점수가 노출되는 상황을 방지하는 용도로 사용됨
1.
기존의 리로스쿨에는 해당기능이 이미 포함되어 있음.
2.
시스템을 만드는 과정에서 생성형AI를 활용하여 기록된 점수를 개별조회가 가능한 시스템으로 변환하는데 그 핵심이 있음

1. 예시작품

1-1. 더미 데이터로 구성된 데이터베이스 예시(수행평가 점수)

1-2. 개별점수조회 사이트 체험하기

학생별 이름과 비밀번호를 정확하게 입력할 경우 개인별 점수가 조회되는 예시
학기초 학생들에게 과목별로 설정한 비밀번호를 구글 설문을 통해 수합할 수 있다면, 매우 나이스한 시스템이 될 것
해당 페이지의 스크립트 분석을 통해서는 다른학생 점수 조회 불가능

2. 기초파일 연습

gemini를 사용한 프롬프트 및 내용 확인

사용 방법

1.
구글 스프레드시트 생성: 새로운 구글 스프레드시트를 만들고 '점수'라는 이름의 시트를 추가합니다.
2.
데이터 입력: A열에는 '이름', B열에는 '비밀번호', C열에는 '점수'를 입력하고 아래에 학생들의 정보를 차례로 기입합니다.
3.
앱스 스크립트 실행:
스프레드시트 상단 메뉴에서 확장 프로그램 > Apps Script를 클릭합니다.
아래에 있는 Code.gsindex.html 코드를 각각의 파일에 붙여넣습니다.
오른쪽 상단의 배포 > 새 배포를 클릭합니다.
'유형 선택'에서 웹 앱을 선택하고 액세스 권한이 있는 사용자를 '모든 사용자'로 설정한 후 배포합니다.
생성된 웹 앱 URL로 접속하여 사용합니다.

2-1. Code.gs (서버 측 스크립트)

참고로 "Google Script" (구글 스크립트)의 약자
언어: 이 파일 안에 작성하는 프로그래밍 언어는 기본적으로 **자바스크립트(JavaScript)
.gs 파일은 **"구글 환경에서 실행되는 자바스크립트 코드 파일"
구글 앱스 스크립트(.gs)는 자바스크립트를 기반으로 구글 서비스에 특화된 기능을 추가한 것
// 웹 앱을 실행할 때 호출되는 기본 함수 function doGet() { return HtmlService.createHtmlOutputFromFile('index'); } /** * 스프레드시트에서 이름과 비밀번호를 확인하고 점수를 반환하는 함수 * @param {string} name - 사용자가 입력한 이름 * @param {string} password - 사용자가 입력한 비밀번호 * @returns {string|number} - 일치하는 정보가 있으면 점수를, 없으면 오류 메시지를 반환 */ function getScore(name, password) { // '점수' 시트의 데이터를 가져옵니다. const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('점수'); // 데이터가 없는 경우를 대비한 예외 처리 if (!sheet) { return '오류: "점수" 시트를 찾을 수 없습니다.'; } const data = sheet.getDataRange().getValues(); // 첫 행(헤더)을 제외하고 데이터를 순회하며 일치하는 정보를 찾습니다. for (let i = 1; i < data.length; i++) { // 이름(A열)과 비밀번호(B열)가 모두 일치하는 경우 if (data[i][0] === name && data[i][1].toString() === password) { return data[i][2]; // 점수(C열)를 반환합니다. } } // 일치하는 정보가 없을 경우 오류 메시지를 반환합니다. return '이름 또는 비밀번호가 일치하지 않습니다.'; }
JavaScript
복사

2-2. index.html (사용자 인터페이스)

<!DOCTYPE html> <html> <head> <base target="_top"> <style> body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f2f5; } #container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); text-align: center; } h2 { color: #333; } input { width: calc(100% - 20px); padding: 10px; margin-top: 10px; border-radius: 4px; border: 1px solid #ddd; } button { width: 100%; padding: 10px; margin-top: 20px; border: none; border-radius: 4px; background-color: #4CAF50; color: white; font-size: 16px; cursor: pointer; } button:hover { background-color: #45a049; } #result { margin-top: 20px; font-size: 1.2em; color: #d32f2f; min-height: 25px; } </style> </head> <body> <div id="container"> <h2>점수 확인</h2> <form id="scoreForm"> <input type="text" id="name" name="name" placeholder="이름을 입력하세요" required> <input type="password" id="password" name="password" placeholder="비밀번호를 입력하세요" required> <button type="submit">확인</button> </form> <div id="result"></div> </div> <script> // 폼 제출 이벤트를 처리합니다. document.getElementById('scoreForm').addEventListener('submit', function(event) { event.preventDefault(); // 기본 폼 제출 동작을 막습니다. const name = document.getElementById('name').value; const password = document.getElementById('password').value; const resultDiv = document.getElementById('result'); resultDiv.textContent = '확인 중...'; // 서버 측 getScore 함수를 호출합니다. google.script.run .withSuccessHandler(function(score) { // 성공적으로 점수를 받아왔을 때 실행됩니다. if (typeof score === 'number') { resultDiv.style.color = 'green'; resultDiv.textContent = name + '님의 점수는 ' + score + '점입니다.'; } else { resultDiv.style.color = 'red'; resultDiv.textContent = score; // 오류 메시지를 표시합니다. } }) .withFailureHandler(function(error) { // 함수 호출에 실패했을 때 실행됩니다. resultDiv.style.color = 'red'; resultDiv.textContent = '오류가 발생했습니다: ' + error.message; }) .getScore(name, password); }); </script> </body> </html>
HTML
복사

2-3. 구글 스프레드 시트 데이터를 통해 시스템 완성하기

1.이름, 비밀번호, 점수가 입력된 스프레드 시트를 준비함
동명이인이 존재할 수 있으므로 이럴 경우 유일한 값을 의미하는 Super Key값을 설정하는 것이 필요함
이후 업그레이드 과정에서 설명할테지만, ‘이름+학번’조합이 우주최강 조합임!
슈퍼 키(Super Key):유일성을 만족하는 키. 예를 들면, {학번 + 이름}, {주민등록번호 + 학번}
Plain Text
복사
2.
해당 시트에서 ‘확장프로그램’- ‘Apps Script’를 선택하기
3.
파일아래 Code.gs와 index.html을 생성하고 위의 코드를 붙여 넣고 저장 후 실행

2-4. 배포하기

1.
‘배포’버튼을 클릭한뒤, 설정을 진행
2.
웹앱에서 ‘나’를 선택한 뒤, 액세스 권한이 있는 사용자를 ‘모든 사용자’로 선택
3.
배포 유형을 ‘웹앱’으로 선택해야 스마트폰에서도 조회가능
4.
배포버튼을 누르면 배포ID 와 URL이 설정되고, URL를 복사하여 배포 가능
5.
이 주소를 공유하면 학생들이 각자의 점수만 프라이빗하게 조회가능!

3. 업그레이드

코드 수정 후에는 반드시 새버전으로 다시 배포하기

3-1. 슈퍼키를 통해 개별조회(전체 점수만)

만약 학년 또는 반에서 동일한 이름이 존재한다면?
다음과 같은 프롬프트를 통해 Code.gs를 수정
나는 학생들의 학번(4자리 또는 5자리)와 학생이름을 슈퍼키로 설정해서 학생들의 최종 점수만 조회가능하게 하고 싶어. A열에는 학번, B열에는 이름, C열에는비밀번호, D열에는 최종점수가 저장되어 있음
즉, 학번, 이름, 비밀번호를 정확하게 입력했을때에만 조회가 가능하도록 점수 조회 시스템을 업그레이드하는 것
// 웹 앱을 실행할 때 호출되는 기본 함수 function doGet() { return HtmlService.createHtmlOutputFromFile('index') .setTitle('학생 점수 조회') // 웹 앱의 제목 설정 .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); // iframe에서 실행 허용 } /** * 스프레드시트에서 학번, 이름, 비밀번호를 확인하고 최종 점수를 반환하는 함수 * @param {string} studentId - 사용자가 입력한 학번 (A열) * @param {string} name - 사용자가 입력한 이름 (B열) * @param {string} password - 사용자가 입력한 비밀번호 (C열) * @returns {string|number} - 일치하는 정보가 있으면 최종 점수를, 없으면 오류 메시지를 반환 */ function getScore(studentId, name, password) { // '점수' 시트의 데이터를 가져옵니다. const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('점수'); // 데이터가 없는 경우를 대비한 예외 처리 if (!sheet) { return '오류: "점수" 시트를 찾을 수 없습니다.'; } const data = sheet.getDataRange().getValues(); // 첫 행(헤더)을 제외하고 데이터를 순회하며 일치하는 정보를 찾습니다. for (let i = 1; i < data.length; i++) { // 학번(A열), 이름(B열), 비밀번호(C열)가 모두 일치하는 경우 // 스프레드시트 데이터는 배열 인덱스 0부터 시작: A열=0, B열=1, C열=2, D열=3 if (data[i][0].toString() === studentId && data[i][1] === name && data[i][2].toString() === password) { return data[i][3]; // 최종 점수(D열)를 반환합니다. } } // 일치하는 정보가 없을 경우 오류 메시지를 반환합니다. return '학번, 이름 또는 비밀번호가 일치하지 않습니다.'; }
JavaScript
복사
<!DOCTYPE html> <html> <head> <base target="_top"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>학생 점수 조회</title> <!-- Tailwind CSS CDN --> <script src="https://cdn.tailwindcss.com"></script> <style> body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; /* Light blue-gray background */ } /* Custom styles for input focus */ input:focus { outline: none; box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5); /* Blue focus ring */ } </style> </head> <body class="flex items-center justify-center min-h-screen p-4"> <div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-md"> <h1 class="text-3xl font-bold text-center text-gray-800 mb-6">학생 점수 조회</h1> <div class="mb-4"> <label for="studentId" class="block text-gray-700 text-sm font-semibold mb-2">학번 (4-5자리):</label> <input type="text" id="studentId" placeholder="학번을 입력하세요" class="shadow-sm appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:border-blue-500 focus:ring-blue-500 transition duration-200"> </div> <div class="mb-4"> <label for="name" class="block text-gray-700 text-sm font-semibold mb-2">이름:</label> <input type="text" id="name" placeholder="이름을 입력하세요" class="shadow-sm appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:border-blue-500 focus:ring-blue-500 transition duration-200"> </div> <div class="mb-6"> <label for="password" class="block text-gray-700 text-sm font-semibold mb-2">비밀번호:</label> <input type="password" id="password" placeholder="비밀번호를 입력하세요" class="shadow-sm appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:border-blue-500 focus:ring-blue-500 transition duration-200"> </div> <button id="getScoreBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg focus:outline-none focus:shadow-outline transition duration-200 ease-in-out transform hover:-translate-y-0.5 hover:scale-105"> 점수 조회 </button> <div id="result" class="mt-6 p-4 rounded-lg text-center text-lg font-semibold" style="display: none;"> <!-- 점수 또는 오류 메시지가 여기에 표시됩니다. --> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const getScoreBtn = document.getElementById('getScoreBtn'); const studentIdInput = document.getElementById('studentId'); const nameInput = document.getElementById('name'); const passwordInput = document.getElementById('password'); const resultDiv = document.getElementById('result'); getScoreBtn.addEventListener('click', function() { const studentId = studentIdInput.value.trim(); const name = nameInput.value.trim(); const password = passwordInput.value.trim(); // 입력 필드 유효성 검사 if (!studentId || !name || !password) { displayResult('모든 필드를 입력해주세요.', 'bg-red-100 text-red-700'); return; } // 학번 형식 유효성 검사 (4자리 또는 5자리 숫자) if (!/^\d{4,5}$/.test(studentId)) { displayResult('학번은 4자리 또는 5자리의 숫자여야 합니다.', 'bg-red-100 text-red-700'); return; } // 로딩 상태 표시 getScoreBtn.textContent = '조회 중...'; getScoreBtn.disabled = true; resultDiv.style.display = 'none'; // 이전 결과 숨기기 // Google Apps Script 함수 호출 google.script.run .withSuccessHandler(function(score) { getScoreBtn.textContent = '점수 조회'; // 버튼 텍스트 복원 getScoreBtn.disabled = false; // 버튼 활성화 if (typeof score === 'number') { displayResult(`당신의 최종 점수는: ${score}점 입니다.`, 'bg-green-100 text-green-700'); } else { displayResult(score, 'bg-red-100 text-red-700'); } }) .withFailureHandler(function(error) { getScoreBtn.textContent = '점수 조회'; // 버튼 텍스트 복원 getScoreBtn.disabled = false; // 버튼 활성화 displayResult(`오류 발생: ${error.message}`, 'bg-red-100 text-red-700'); console.error('Apps Script Error:', error); }) .getScore(studentId, name, password); }); // 결과 메시지를 표시하고 스타일을 적용하는 헬퍼 함수 function displayResult(message, className) { resultDiv.textContent = message; resultDiv.className = `mt-6 p-4 rounded-lg text-center text-lg font-semibold ${className}`; resultDiv.style.display = 'block'; } }); </script> </body> </html>
HTML
복사

3-2. 슈퍼키를 통해 개별조회(점수세부영역까지 모두)

학생들의 최종점수 뿐만 아니라 세부 점수를 모두 조회할 수있도록 수정
또한 세부 영역의 갯수를 2개 또는 3개로 고정하지 않고, 유동적으로 변경가능한 상황까지 고려하여 업그레이하기
화면에 보여지는 index.html의 구성요소가 세부영역의 갯수에 따라 동적으로 변경될 수 있어야 함
// 웹 앱을 실행할 때 호출되는 기본 함수 function doGet() { return HtmlService.createHtmlOutputFromFile('index') .setTitle('학생 점수 조회') // 웹 앱의 제목 설정 .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); // iframe에서 실행 허용 } /** * 스프레드시트에서 학번, 이름, 비밀번호를 확인하고 최종 점수 및 영역별 점수를 반환하는 함수 * @param {string} studentId - 사용자가 입력한 학번 (A열) * @param {string} name - 사용자가 입력한 이름 (B열) * @param {string} password - 사용자가 입력한 비밀번호 (C열) * @returns {object|string} - 일치하는 정보가 있으면 { finalScore: number, areas: object } 객체를, 없으면 오류 메시지를 반환 */ function getScore(studentId, name, password) { // '점수' 시트의 데이터를 가져옵니다. const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('점수'); // 데이터가 없는 경우를 대비한 예외 처리 if (!sheet) { return '오류: "점수" 시트를 찾을 수 없습니다.'; } const data = sheet.getDataRange().getValues(); // 데이터가 없거나 헤더만 있는 경우 처리 if (data.length < 2) { return '오류: 스프레드시트에 데이터가 없습니다.'; } // 헤더 행에서 영역 점수 열의 시작 인덱스를 찾습니다. // 최종 점수(D열, 인덱스 3) 다음부터 영역 점수가 시작됩니다. const header = data[0]; const finalScoreColIndex = 3; // D열 const areaScoresStartIndex = finalScoreColIndex + 1; // E열부터 시작 // 첫 행(헤더)을 제외하고 데이터를 순회하며 일치하는 정보를 찾습니다. for (let i = 1; i < data.length; i++) { // 학번(A열), 이름(B열), 비밀번호(C열)가 모두 일치하는 경우 // 스프레드시트 데이터는 배열 인덱스 0부터 시작: A열=0, B열=1, C열=2, D열=3 if (data[i][0].toString() === studentId && data[i][1] === name && data[i][2].toString() === password) { const finalScore = data[i][finalScoreColIndex]; // 최종 점수(D열) const areaScores = {}; // 영역별 점수를 동적으로 추출합니다. // E열부터 시작하여 헤더에 '영역'으로 시작하는 모든 열을 포함합니다. for (let j = areaScoresStartIndex; j < header.length; j++) { const headerName = header[j]; if (headerName && typeof headerName === 'string' && headerName.startsWith('영역')) { areaScores[headerName] = data[i][j]; } } // 최종 점수와 영역별 점수를 객체 형태로 반환합니다. return { finalScore: finalScore, areas: areaScores }; } } // 일치하는 정보가 없을 경우 오류 메시지를 반환합니다. return '학번, 이름 또는 비밀번호가 일치하지 않습니다.'; }
JavaScript
복사
<!DOCTYPE html> <html> <head> <base target="_top"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>학생 점수 조회</title> <!-- Tailwind CSS CDN --> <script src="https://cdn.tailwindcss.com"></script> <style> body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; /* Light blue-gray background */ } /* Custom styles for input focus */ input:focus { outline: none; box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5); /* Blue focus ring */ } </style> </head> <body class="flex items-center justify-center min-h-screen p-4"> <div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-md"> <h1 class="text-3xl font-bold text-center text-gray-800 mb-6">학생 점수 조회</h1> <div class="mb-4"> <label for="studentId" class="block text-gray-700 text-sm font-semibold mb-2">학번 (45자리):</label> <input type="text" id="studentId" placeholder="학번을 입력하세요" class="shadow-sm appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:border-blue-500 focus:ring-blue-500 transition duration-200"> </div> <div class="mb-4"> <label for="name" class="block text-gray-700 text-sm font-semibold mb-2">이름:</label> <input type="text" id="name" placeholder="이름을 입력하세요" class="shadow-sm appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:border-blue-500 focus:ring-blue-500 transition duration-200"> </div> <div class="mb-6"> <label for="password" class="block text-gray-700 text-sm font-semibold mb-2">비밀번호:</label> <input type="password" id="password" placeholder="비밀번호를 입력하세요" class="shadow-sm appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:border-blue-500 focus:ring-blue-500 transition duration-200"> </div> <button id="getScoreBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg focus:outline-none focus:shadow-outline transition duration-200 ease-in-out transform hover:-translate-y-0.5 hover:scale-105"> 점수 조회 </button> <div id="result" class="mt-6 p-4 rounded-lg text-center text-lg font-semibold" style="display: none;"> <!-- 점수 또는 오류 메시지가 여기에 표시됩니다. --> </div> <div id="areaScores" class="mt-4 p-4 rounded-lg bg-gray-50 border border-gray-200" style="display: none;"> <h3 class="text-xl font-bold text-gray-800 mb-3 text-center">영역별 점수</h3> <ul id="areaScoresList" class="list-disc list-inside text-left text-gray-700"> <!-- 영역별 점수가 여기에 동적으로 추가됩니다. --> </ul> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const getScoreBtn = document.getElementById('getScoreBtn'); const studentIdInput = document.getElementById('studentId'); const nameInput = document.getElementById('name'); const passwordInput = document.getElementById('password'); const resultDiv = document.getElementById('result'); const areaScoresDiv = document.getElementById('areaScores'); const areaScoresList = document.getElementById('areaScoresList'); getScoreBtn.addEventListener('click', function() { const studentId = studentIdInput.value.trim(); const name = nameInput.value.trim(); const password = passwordInput.value.trim(); // 입력 필드 유효성 검사 if (!studentId || !name || !password) { displayResult('모든 필드를 입력해주세요.', 'bg-red-100 text-red-700'); hideAreaScores(); return; } // 학번 형식 유효성 검사 (4자리 또는 5자리 숫자) if (!/^\d{4,5}$/.test(studentId)) { displayResult('학번은 4자리 또는 5자리의 숫자여야 합니다.', 'bg-red-100 text-red-700'); hideAreaScores(); return; } // 로딩 상태 표시 getScoreBtn.textContent = '조회 중...'; getScoreBtn.disabled = true; resultDiv.style.display = 'none'; // 이전 결과 숨기기 hideAreaScores(); // 영역별 점수 숨기기 // Google Apps Script 함수 호출 google.script.run .withSuccessHandler(function(response) { getScoreBtn.textContent = '점수 조회'; // 버튼 텍스트 복원 getScoreBtn.disabled = false; // 버튼 활성화 if (typeof response === 'object' && response !== null && response.finalScore !== undefined) { // 성공적으로 점수 데이터를 받은 경우 displayResult(`당신의 최종 점수는: ${response.finalScore}점 입니다.`, 'bg-green-100 text-green-700'); displayAreaScores(response.areas); } else { // 오류 메시지를 받은 경우 displayResult(response, 'bg-red-100 text-red-700'); hideAreaScores(); } }) .withFailureHandler(function(error) { getScoreBtn.textContent = '점수 조회'; // 버튼 텍스트 복원 getScoreBtn.disabled = false; // 버튼 활성화 displayResult(`오류 발생: ${error.message}`, 'bg-red-100 text-red-700'); hideAreaScores(); console.error('Apps Script Error:', error); }) .getScore(studentId, name, password); }); // 결과 메시지를 표시하고 스타일을 적용하는 헬퍼 함수 function displayResult(message, className) { resultDiv.textContent = message; resultDiv.className = `mt-6 p-4 rounded-lg text-center text-lg font-semibold ${className}`; resultDiv.style.display = 'block'; } // 영역별 점수를 표시하는 헬퍼 함수 function displayAreaScores(areas) { areaScoresList.innerHTML = ''; // 기존 목록 초기화 let hasAreas = false; // 영역 이름을 추출하고 숫자 부분을 기준으로 정렬합니다. const sortedAreaNames = Object.keys(areas).sort((a, b) => { // '영역' 다음의 숫자 부분을 추출합니다. (예: '영역1' -> 1, '영역10' -> 10) const numA = parseInt(a.replace('영역', ''), 10); const numB = parseInt(b.replace('영역', ''), 10); return numA - numB; }); for (const areaName of sortedAreaNames) { if (Object.hasOwnProperty.call(areas, areaName)) { const score = areas[areaName]; const li = document.createElement('li'); // 점수가 숫자인 경우에만 '점'을 붙입니다. li.textContent = `${areaName}: ${isNaN(score) ? score : score + '점'}`; areaScoresList.appendChild(li); hasAreas = true; } } if (hasAreas) { areaScoresDiv.style.display = 'block'; } else { areaScoresDiv.style.display = 'none'; } } // 영역별 점수 섹션을 숨기는 헬퍼 함수 function hideAreaScores() { areaScoresDiv.style.display = 'none'; areaScoresList.innerHTML = ''; } }); </script> </body> </html>
HTML
복사