# 🍒 구글 스프레드시트 × 그라운드 API 연동 가이드

**🥭 샘플 스프레드 시트  👉🏻 ****[다운로드 ](https://docs.google.com/spreadsheets/d/1sEe8PqzcyBAwWe9Yr7H7pEzNkZrGCxhVew6LUyepbUg/edit?usp=sharing)**

## 📋 목차

1. [개요](https://#%EA%B0%9C%EC%9A%94)

2. [사전 준비](https://#%EC%82%AC%EC%A0%84-%EC%A4%80%EB%B9%84)

3. [버튼으로 일괄 포인트 부여](https://#%EB%B2%84%ED%8A%BC%EC%9C%BC%EB%A1%9C-%EC%9D%BC%EA%B4%84-%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EB%B6%80%EC%97%AC)

4. [문제 해결](https://#%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0)

---

## 개요

구글 스프레드시트의 Apps Script를 사용하여 그라운드 API와 연동하면:

- ✅ 학생 명단에서 직접 포인트 부여

- ✅ 버튼 클릭만으로 일괄 포인트 처리

- ✅ 출석, 과제, 퀴즈 점수를 자동으로 포인트로 변환

- ✅ 진행 상황 실시간 표시

---

## 사전 준비

### 1. 그라운드 API 키 발급

1. [그라운드 웹사이트](https://growndcard.com) 로그인

2. **내 정보** → **API 키 관리** 탭

3. **새 API 키 생성** 클릭

4. API 키 복사 (예: `sk_live_xxx...`)

### 2. 클래스 ID 확인

1. 그라운드 웹사이트 → **클래스 관리**

2. 포인트를 부여할 클래스의 **ID** 복사 (예: `NP0hetJ3wyQKFtRnFeftmPiy8Dl3_2`)

### 3. 학생 번호 확인

- 그라운드에서 각 학생에게 부여된 **번호(code)** 확인

- 스프레드시트의 학생 목록과 매칭

---

## 버튼으로 일괄 포인트 부여

### ✨ 특징

- 여러 학생에게 동시에 포인트 부여

- 선택한 행만 처리 가능

- 진행 상황 실시간 표시

- 상단 메뉴에서 버튼 클릭만으로 실행

### 📝 스프레드시트 구성

| A | B | C | D | E | F |
| --- | --- | --- | --- | --- | --- |
| **선택** | **학생번호** | **이름** | **포인트** | **설명** | **상태** |
| ☑️ | 1 | 김철수 | 10 | 출석 | 대기중 |
| ☑️ | 2 | 이영희 | 10 | 출석 | 대기중 |
| ☐ | 3 | 박민수 | 10 | 출석 | 대기중 |

### 🔧 Apps Script 코드

```
/**
 * 그라운드 API 설정
 * ⚠️ 실제 API 키와 클래스 ID로 변경하세요!
 */
const GROWND_API_KEY = 'sk_live_your_api_key_here'; // 발급받은 API 키
const GROWND_CLASS_ID = 'your_class_id_here'; // 클래스 ID
const GROWND_API_URL = 'https://growndcard.com/api/v1'; 

/**
 * 메뉴에 커스텀 버튼 추가
 */
function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('🎯 그라운드')
    .addItem('선택한 학생에게 포인트 부여', 'awardPointsToSelected')
    .addItem('전체 학생에게 포인트 부여', 'awardPointsToAll')
    .addSeparator()
    .addItem('설정', 'showSettings')
    .addToUi();
}

/**
 * 선택한 학생에게 포인트 부여
 */
function awardPointsToSelected() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const ui = SpreadsheetApp.getUi();
  
  // 확인 대화상자
  const response = ui.alert(
    '포인트 부여 확인',
    '선택된 학생들에게 포인트를 부여하시겠습니까?',
    ui.ButtonSet.YES_NO
  );
  
  if (response !== ui.Button.YES) {
    return;
  }

  // 데이터 범위 가져오기 (2행부터 시작, 헤더 제외)
  const dataRange = sheet.getRange(2, 1, sheet.getLastRow() - 1, 6);
  const data = dataRange.getValues();
  
  let successCount = 0;
  let failCount = 0;

  // 각 행 처리
  for (let i = 0; i < data.length; i++) {
    const [checked, studentCode, name, points, description, status] = data[i];
    
    // 체크박스가 선택된 행만 처리
    if (checked === true) {
      // 상태 업데이트: 처리 중
      sheet.getRange(i + 2, 6).setValue('⏳ 처리 중...');
      SpreadsheetApp.flush(); // 즉시 화면 업데이트
      
      try {
        const result = awardPointToStudent(studentCode, points, description);
        
        if (result.success) {
          sheet.getRange(i + 2, 6).setValue(`✅ ${points}P 부여 완료`);
          successCount++;
        } else {
          sheet.getRange(i + 2, 6).setValue(`❌ ${result.error}`);
          failCount++;
        }
      } catch (error) {
        sheet.getRange(i + 2, 6).setValue(`❌ ${error.message}`);
        failCount++;
      }
      
      SpreadsheetApp.flush();
    }
  }

  // 결과 알림
  ui.alert(
    '포인트 부여 완료',
    `성공: ${successCount}명\n실패: ${failCount}명`,
    ui.ButtonSet.OK
  );
}

/**
 * 전체 학생에게 포인트 부여
 */
function awardPointsToAll() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const ui = SpreadsheetApp.getUi();
  
  // 확인 대화상자
  const response = ui.alert(
    '전체 포인트 부여 확인',
    '전체 학생에게 포인트를 부여하시겠습니까?',
    ui.ButtonSet.YES_NO
  );
  
  if (response !== ui.Button.YES) {
    return;
  }

  const dataRange = sheet.getRange(2, 2, sheet.getLastRow() - 1, 5); // 체크박스 제외
  const data = dataRange.getValues();
  
  let successCount = 0;
  let failCount = 0;

  for (let i = 0; i < data.length; i++) {
    const [studentCode, name, points, description, status] = data[i];
    
    if (studentCode && points) {
      sheet.getRange(i + 2, 6).setValue('⏳ 처리 중...');
      SpreadsheetApp.flush();
      
      try {
        const result = awardPointToStudent(studentCode, points, description);
        
        if (result.success) {
          sheet.getRange(i + 2, 6).setValue(`✅ ${points}P 부여 완료`);
          successCount++;
        } else {
          sheet.getRange(i + 2, 6).setValue(`❌ ${result.error}`);
          failCount++;
        }
      } catch (error) {
        sheet.getRange(i + 2, 6).setValue(`❌ ${error.message}`);
        failCount++;
      }
      
      SpreadsheetApp.flush();
    }
  }

  ui.alert(
    '포인트 부여 완료',
    `성공: ${successCount}명\n실패: ${failCount}명`,
    ui.ButtonSet.OK
  );
}

/**
 * 개별 학생에게 포인트 부여 (내부 함수)
 */
function awardPointToStudent(studentCode, points, description) {
  const url = `${GROWND_API_URL}/classes/${GROWND_CLASS_ID}/students/${studentCode}/points`;
  
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'X-API-Key': GROWND_API_KEY
    },
    payload: JSON.stringify({
      type: 'reward',
      points: points,
      description: description || '스프레드시트에서 부여',
      source: 'google_sheets'
    }),
    muteHttpExceptions: true
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const statusCode = response.getResponseCode();
    const contentText = response.getContentText();
    
    // HTML 응답 체크 (에러 페이지 방지)
    if (contentText.startsWith('<')) {
      console.error('HTML 응답:', contentText.substring(0, 200));
      return { 
        success: false, 
        error: `API 응답 오류 (상태 ${statusCode}). URL을 확인하세요.` 
      };
    }
    
    const result = JSON.parse(contentText);
    
    return result.success 
      ? { success: true } 
      : { success: false, error: result.error.message };
      
  } catch (error) {
    console.error('API 호출 에러:', error);
    return { success: false, error: error.message };
  }
}

/**
 * 설정 대화상자
 */
function showSettings() {
  const ui = SpreadsheetApp.getUi();
  ui.alert(
    '⚙️ API 설정',
    `현재 설정:\n\n` +
    `API 키: ${GROWND_API_KEY.substring(0, 15)}...\n` +
    `클래스 ID: ${GROWND_CLASS_ID}\n\n` +
    `설정을 변경하려면 Apps Script 코드를 수정하세요.`,
    ui.ButtonSet.OK
  );
}
```

### 📖 설정 방법

### 1단계: Apps Script 열기

1. 구글 스프레드시트에서 상단 메뉴: **확장 프로그램** → **Apps Script**

2. 새 창이 열리면 기존 코드 삭제

### 2단계: 코드 복사 및 설정

1. 위의 전체 코드를 복사하여 붙여넣기

2. **중요**: 다음 두 줄을 실제 값으로 수정:

```
const GROWND_API_KEY = 'sk_live_xxx...'; // 발급받은 실제 API 키
const GROWND_CLASS_ID = 'NP0hetJ3wyQKFtRnFeftmPiy8Dl3_2'; // 실제 클래스 ID
```

### 3단계: 저장 및 권한 부여

1. 💾 **저장** 버튼 클릭 (또는 Ctrl+S / Cmd+S)

2. 프로젝트 이름 입력 (예: "그라운드 포인트 관리")

3. Apps Script 탭 닫기

### 4단계: 스프레드시트 구성

1. **스프레드시트를 닫았다가 다시 열기** (새로고침)

2. 잠시 기다리면 상단에 **"🎯 그라운드"** 메뉴가 나타남

3. 스프레드시트에 다음과 같이 구성:

**A열**: 체크박스 삽입

- **삽입** → **체크박스** 클릭

- 각 학생 행에 체크박스 추가

**B열**: 학생번호 (숫자, 예: 1, 2, 3...)
**C열**: 학생이름
**D열**: 부여할 포인트 (숫자)
**E열**: 포인트 설명 (텍스트)
**F열**: 상태 (자동으로 채워짐, 비워두기)

### 5단계: 첫 실행 시 권한 부여

1. **🎯 그라운드** → **선택한 학생에게 포인트 부여** 클릭

2. **권한 검토** 팝업이 나타나면 클릭

3. 본인 구글 계정 선택

4. **고급** 클릭 → **안전하지 않은 페이지로 이동** 클릭

5. **허용** 클릭

### 6단계: 사용하기

1. 포인트를 부여할 학생의 **체크박스 선택**

2. **🎯 그라운드** → **선택한 학생에게 포인트 부여** 클릭

3. 확인 대화상자에서 **예** 클릭

4. 실시간으로 처리 상황 확인

---

## 문제 해결

### ❌ "권한이 없습니다" 오류

**원인**: Apps Script 권한이 부여되지 않음

**해결**:

1. Apps Script 실행 시 **권한 검토** 클릭

2. 본인 구글 계정 선택

3. **고급** → **안전하지 않은 페이지로 이동** 클릭

4. **허용** 클릭

### ❌ "API 키 인증 실패" 오류

**원인**: API 키가 잘못되었거나 만료됨

**해결**:

1. 그라운드 웹사이트에서 API 키 확인

2. Apps Script 코드의 `GROWND_API_KEY` 값 업데이트

3. API 키가 비활성화되지 않았는지 확인

### ❌ "학생을 찾을 수 없습니다" 오류

**원인**: 학생 번호가 잘못됨

**해결**:

1. 그라운드 웹사이트에서 정확한 학생 번호 확인

2. 스프레드시트의 학생 번호와 일치하는지 확인

3. 학생 번호는 **숫자**여야 함 (문자열 X)

4. B열 전체를 선택 → 우클릭 → **서식** → **숫자** → **숫자** 선택

### ❌ "클래스 권한이 없습니다" 오류

**원인**: API 키가 해당 클래스에 접근 권한이 없음

**해결**:

1. 그라운드 웹사이트 → **내 정보** → **API 키 관리**

2. 해당 API 키의 권한에 클래스가 포함되어 있는지 확인

3. 필요시 API 키를 삭제하고 재생성

### ❌ "일일 호출 제한 초과" 오류

**원인**: 하루 500회 API 호출 제한 도달

**해결**:

1. 내일까지 대기 (매일 자정 초기화)

2. 또는 여러 API 키를 생성하여 분산 사용

3. 불필요한 중복 호출 제거

### ❌ 상단에 "🎯 그라운드" 메뉴가 안 보임

**원인**: 코드가 제대로 저장되지 않았거나 새로고침 필요

**해결**:

1. Apps Script에서 코드가 저장되었는지 확인

2. 스프레드시트를 **완전히 닫기**

3. 다시 열기 (최대 30초 대기)

4. 여전히 안 보이면 Apps Script에서 `onOpen` 함수를 **직접 실행**

### ❌ 함수 실행이 너무 느림

**원인**: 많은 학생에게 순차적으로 API 호출

**해결**:

- 학생 수가 많을 경우 10-20명씩 나눠서 처리

- 또는 코드에 배치 처리 추가 (rate limit 방지용 대기 시간 포함)

---

## 📌 활용 팁

### 1️⃣ 출석 체크 시트

| A | B | C | D | E | F |
| --- | --- | --- | --- | --- | --- |
| **선택** | **학생번호** | **이름** | **포인트** | **설명** | **상태** |
| ☑️ | 1 | 김철수 | 5 | 2025-01-15 출석 |  |
| ☑️ | 2 | 이영희 | 5 | 2025-01-15 출석 |  |
| ☐ | 3 | 박민수 | 0 | 결석 |  |

- 출석한 학생만 체크박스 선택

- D열에 출석 포인트 (예: 5점) 입력

- E열에 날짜 + "출석" 입력

- **🎯 그라운드** → **선택한 학생에게 포인트 부여**

### 2️⃣ 퀴즈/과제 점수

| A | B | C | D | E | F |
| --- | --- | --- | --- | --- | --- |
| **선택** | **학생번호** | **이름** | **포인트** | **설명** | **상태** |
| ☑️ | 1 | 김철수 | 20 | 수학 퀴즈 A등급 |  |
| ☑️ | 2 | 이영희 | 15 | 수학 퀴즈 B등급 |  |
| ☑️ | 3 | 박민수 | 10 | 수학 퀴즈 C등급 |  |

- 전체 학생에게 차등 포인트 부여

- **🎯 그라운드** → **전체 학생에게 포인트 부여**

### 3️⃣ 일괄 포인트 부여

전체 학생에게 동일한 포인트를 부여할 때:

- 체크박스 없이 B~E열만 채우기

- **🎯 그라운드** → **전체 학생에게 포인트 부여**

---

## 🎯 마무리

이제 구글 스프레드시트에서 그라운드 포인트를 쉽게 관리할 수 있습니다!

### 추천 사용 시나리오

- ✅ **출석 관리**: 매일 출석 체크 → 버튼 클릭으로 포인트 부여

- ✅ **퀴즈/시험**: 점수 입력 → 등급별 포인트 차등 부여

- ✅ **과제 제출**: 제출 여부 체크 → 일괄 포인트 부여

- ✅ **보너스 포인트**: 특별 활동, 우수 학생 포인트 지급

### 참고

- 📖 API 문서:  [https://slashpage.com/grownd/grownd_API](https://slashpage.com/grownd/grownd_API)

---

For the site tree, see the [root Markdown](https://slashpage.com/grownd.md).
