# 🐣 그라운드 포인트 API 사용 가이드

👉🏻 그라운드에서 학생에게 포인트를 부여하는 포인트 API 기능을 설명합니다.

## 목차

1. [시작하기](https://#%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0)

2. [API 키 발급](https://#api-%ED%82%A4-%EB%B0%9C%EA%B8%89)

3. [인증 방식](https://#%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D)

4. [포인트 부여 API](https://#%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EB%B6%80%EC%97%AC-api)

5. [에러 처리](https://#%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC)

6. [코드 예제](https://#%EC%BD%94%EB%93%9C-%EC%98%88%EC%A0%9C)

7. [제한 사항](https://#%EC%A0%9C%ED%95%9C-%EC%82%AC%ED%95%AD)

---

## 시작하기

그라운드 포인트 API를 사용하면 외부 애플리케이션에서 학생들에게 포인트를 부여할 수 있습니다. 퀴즈 앱, 게임, 출석 체크 시스템 등 다양한 서비스와 연동이 가능합니다.

### 기본 정보

- **Base URL**: `[https://growndcard.com](https://growndcard.com)` 

- **프로토콜**: HTTPS

- **인증 방식**: API Key (Header)

---

## API 키 발급

### 1. 그라운드에 로그인

교사 계정으로 [그라운드](https://growndcard.com)에 로그인합니다.

### 2. 내 정보 페이지 이동

우측 상단 프로필 → **내 정보** 클릭

### 3. API 키 관리 탭

**API 키 관리** 탭을 선택합니다.

### 4. 새 API 키 생성

1. **"새 API 키 생성"** 버튼 클릭

2. 다음 정보 입력:

- **API 키 이름**: 애플리케이션 이름 (예: "출석체크 앱")

- **만료 기간**: 선택 (무제한 또는 30일/90일/180일/1년)

- **권한 설정**: 포인트 부여 권한 선택

- **호출 제한**: 기본값(분당 60회, 일일 500회)

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

### 5. API 키 저장

⚠️ **중요**: API 키는 **한 번만 표시**됩니다!

```
sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o123
```

반드시 안전한 곳에 복사하여 보관하세요.

### 제한 사항

- 교사 계정당 **최대 3개**의 API 키 생성 가능

- 일일 최대 **500회** 호출 제한

---

## 인증 방식

모든 API 요청에는 `X-API-Key` 헤더가 필요합니다.

```
X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
```

---

## 포인트 부여 API

### 엔드포인트

```
POST /api/v1/classes/{classId}/students/{studentCode}/points
```

클래스 id 확인 : 클래스별로 다른 클래스 id가 부여됩니다. → [클래스 정보] 메뉴에서 확인

### URL 파라미터

| 파라미터 | 타입 | 설명 | 예시 |
| --- | --- | --- | --- |
| `classId` | string | 클래스 ID (그라운드에서 확인) | `NP0hetJ3wyQKFtRnFeftmPiy8Dl7_2` |
| `studentCode` | number | 학생 번호 (출석번호) | `2` |

> 💡 **classId 확인 방법**: 그라운드 → 클래스 관리 → 클래스 카드에서 "ID" 확인

### 요청 헤더

| 헤더 | 값 | 필수 |
| --- | --- | --- |
| `Content-Type` | `application/json` | ✅ |
| `X-API-Key` | 발급받은 API 키 | ✅ |

### 요청 본문 (JSON)

| 필드 | 타입 | 필수 | 설명 | 예시 |
| --- | --- | --- | --- | --- |
| `type` | string | ✅ | 포인트 종류 | `"reward(+포인트)"` 또는 `"penalty(-포인트)"` |
| `points` | number | ✅ | 포인트 값 (0.01~1000) | `10.5` |
| `description` | string | ✅ | 포인트 설명 (1~500자) | `"퀴즈 정답"` |

### 요청 예시

```
{
  "type": "reward",
  "points": 10,
  "description": "타이핑 게임 클리어"
  }
}
```

### 성공 응답 (200 OK)

```
{
  "success": true,
  "data": {
    "recordId": "rec_xyz789",
    "studentId": "stu_abc123",
    "studentCode": 2,
    "type": "reward",
    "pointsAwarded": 10,
    "bonusApplied": 0,
    "totalPoints": 150.5,
    "currentLevel": 5,
    "leveledUp": false,
    "createdAt": "2025-11-12T01:23:45.000Z"
  },
  "message": "포인트가 성공적으로 반영되었습니다."
}
```

### 응답 필드 설명

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `recordId` | string | 포인트 기록 ID |
| `studentId` | string | 학생의 내부 ID |
| `studentCode` | number | 학생 번호 |
| `type` | string | 포인트 종류 |
| `pointsAwarded` | number | 실제 부여된 포인트 |
| `bonusApplied` | number | 적용된 보너스 배율 (0이면 보너스 없음) |
| `totalPoints` | number | 부여 후 학생의 총 포인트 |
| `currentLevel` | number | 학생의 현재 레벨 |
| `leveledUp` | boolean | 레벨업 여부 |
| `createdAt` | string | 포인트 부여 시각 (ISO 8601) |

---

## 에러 처리

### 에러 응답 형식

```
{
  "success": false,
  "error": {
    "code": "error_code",
    "message": "에러 메시지",
    "details": {}
  }
}
```

### 에러 코드

| 상태 코드 | 에러 코드 | 설명 | 해결 방법 |
| --- | --- | --- | --- |
| 400 | `invalid_type` | type이 올바르지 않음 | `"reward"` 또는 `"penalty"` 사용 |
| 400 | `invalid_points` | points 값이 범위를 벗어남 | 0.01~1000 사이 값 사용 |
| 400 | `invalid_description` | description이 비어있거나 너무 김 | 1~500자 사이 문자열 사용 |
| 400 | `invalid_student_code` | 학생 번호가 제공되지 않음 | studentCode 확인 |
| 401 | `unauthorized` | API 키가 없거나 유효하지 않음 | API 키 확인, X-API-Key 헤더 확인 |
| 403 | `forbidden` | 해당 클래스 접근 권한 없음 | classId 확인 |
| 404 | `student_not_found` | 학생을 찾을 수 없음 | studentCode 확인 |
| 429 | `rate_limit_exceeded` | 호출 제한 초과 | 잠시 후 재시도 |
| 500 | `internal_error` | 서버 내부 오류 | 잠시 후 재시도, 반복되면 문의 |

### 에러 응답 예시

```
{
  "success": false,
  "error": {
    "code": "student_not_found",
    "message": "해당 학생 번호에 해당하는 학생을 찾을 수 없습니다.",
    "details": {
      "classId": "NP0hetJ3wyQKFtRnFeftmPiy8Dl3_2",
      "studentCode": 99
    }
  }
}
```

---

## 코드 예제

### cURL

```
curl -X POST \
  "https://growndcard.com/api/v1/classes/NP0hetJ3wyQKFtRnFeftmPiy8Dl2_2/students/2/points" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5123" \
  -d '{
    "type": "reward",
    "points": 10,
    "description": "퀴즈 정답"
  }'
```

### JavaScript (Node.js)

```
const fetch = require('node-fetch');

const API_KEY = 'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p2';
const BASE_URL = 'https://growndcard.com';
const CLASS_ID = 'NP0hetJ3wyQKFtRnFeftmPiy8Dl3_2';

async function awardPoints(studentCode, points, description) {
  try {
    const response = await fetch(
      `${BASE_URL}/api/v1/classes/${CLASS_ID}/students/${studentCode}/points`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': API_KEY
        },
        body: JSON.stringify({
          type: 'reward',
          points: points,
          description: description
        })
      }
    );

    const data = await response.json();

    if (!response.ok) {
      throw new Error(`API Error: ${data.error.message}`);
    }

    console.log('포인트 부여 성공:', data.data);
    return data.data;
  } catch (error) {
    console.error('포인트 부여 실패:', error.message);
    throw error;
  }
}

// 사용 예시
awardPoints(2, 10, '타이핑 게임 클리어')
  .then(result => console.log('결과:', result))
  .catch(error => console.error('에러:', error));
```

### Python

```
import requests
import json

API_KEY = 'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
BASE_URL = 'https://growndcard.com'
CLASS_ID = 'NP0hetJ3wyQKFtRnFeftmPiy8Dl2_2'

def award_points(student_code, points, description):
    """학생에게 포인트 부여"""
    url = f'{BASE_URL}/api/v1/classes/{CLASS_ID}/students/{student_code}/points'
    
    headers = {
        'Content-Type': 'application/json',
        'X-API-Key': API_KEY
    }
    
    payload = {
        'type': 'reward',
        'points': points,
        'description': description
    }
    
    try:
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()
        
        data = response.json()
        print(f"포인트 부여 성공: {data['data']}")
        return data['data']
        
    except requests.exceptions.HTTPError as e:
        error_data = response.json()
        print(f"API 에러: {error_data['error']['message']}")
        raise
    except Exception as e:
        print(f"요청 실패: {str(e)}")
        raise

# 사용 예시
if __name__ == '__main__':
    result = award_points(2, 10, '퀴즈 정답')
    print(f"총 포인트: {result['totalPoints']}")
```

### TypeScript (React/Next.js)

```
interface AwardPointsRequest {
  type: 'reward' | 'penalty';
  points: number;
  description: string;
  source?: string;
  metadata?: Record<string, any>;
}

interface AwardPointsResponse {
  success: boolean;
  data?: {
    recordId: string;
    studentCode: number;
    pointsAwarded: number;
    totalPoints: number;
    currentLevel: number;
    leveledUp: boolean;
    createdAt: string;
  };
  message?: string;
}

class GroundApiClient {
  private apiKey: string;
  private baseUrl: string;
  private classId: string;

  constructor(apiKey: string, classId: string) {
    this.apiKey = apiKey;
    this.classId = classId;
    this.baseUrl = 'https://growndcard.com';
  }

  async awardPoints(
    studentCode: number,
    request: AwardPointsRequest
  ): Promise<AwardPointsResponse> {
    const url = `${this.baseUrl}/api/v1/classes/${this.classId}/students/${studentCode}/points`;

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': this.apiKey
      },
      body: JSON.stringify(request)
    });

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error?.message || 'API 호출 실패');
    }

    return data;
  }
}

// 사용 예시
const client = new GroundApiClient(
  'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p3',
  'NP0hetJ3wyQKFtRnFeftmPiy8Dl3_2'
);

async function example() {
  try {
    const result = await client.awardPoints(2, {
      type: 'reward',
      points: 10,
      description: '타이핑 게임 클리어'
    });

    console.log('포인트 부여 성공:', result.data);
  } catch (error) {
    console.error('포인트 부여 실패:', error);
  }
}
```

### PHP

```
<?php

function awardPoints($studentCode, $points, $description) {
    $apiKey = 'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p1';
    $classId = 'NP0hetJ3wyQKFtRnFeftmPiy8Dl3_2';
    $baseUrl = 'https://growndcard.com';
    
    $url = "{$baseUrl}/api/v1/classes/{$classId}/students/{$studentCode}/points";
    
    $data = [
        'type' => 'reward',
        'points' => $points,
        'description' => $description
    ];
    
    $options = [
        'http' => [
            'method' => 'POST',
            'header' => [
                'Content-Type: application/json',
                "X-API-Key: {$apiKey}"
            ],
            'content' => json_encode($data)
        ]
    ];
    
    $context = stream_context_create($options);
    $response = file_get_contents($url, false, $context);
    
    if ($response === false) {
        throw new Exception('API 호출 실패');
    }
    
    $result = json_decode($response, true);
    
    if (!$result['success']) {
        throw new Exception($result['error']['message']);
    }
    
    return $result['data'];
}

// 사용 예시
try {
    $result = awardPoints(2, 10, '퀴즈 정답');
    echo "포인트 부여 성공: 총 포인트 = {$result['totalPoints']}\n";
} catch (Exception $e) {
    echo "에러: " . $e->getMessage() . "\n";
}
```

---

## 제한 사항

### API 키 제한

- 교사 계정당 **최대 3개** API 키 생성 가능

- API 키는 생성 시 **한 번만 표시**됨 (분실 시 재발급 필요)

### 호출 제한 (Rate Limit)

- **분당 최대**: 60회

- **일일 최대**: 500회

- 제한 초과 시 `429 Too Many Requests` 응답

### 포인트 제한

- 한 번에 부여 가능한 포인트: **0.01 ~ 1000점**

- 설명(description): **1 ~ 500자**

### 보안 권장사항

1. ⚠️ API 키를 **절대 공개 저장소**에 커밋하지 마세요

2. ✅ 환경 변수(`.env`)에 저장하세요

3. ✅ 프론트엔드에 노출하지 말고 **백엔드에서만** 사용하세요

4. ✅ 정기적으로 API 키를 **재발급**하세요

---

## FAQ

### Q1. classId는 어디서 확인하나요?

**A**: 그라운드 → 클래스 관리 → 각 클래스 카드의 "ID" 필드에서 확인할 수 있습니다.

### Q2. 학생 번호(studentCode)는 무엇인가요?

**A**: 학생의 출석번호입니다. 그라운드의 학생 목록에서 확인할 수 있습니다.

### Q3. API 키를 분실했어요!

**A**: API 키는 재조회가 불가능합니다. 기존 키를 삭제하고 새로운 키를 발급받으세요.

### Q4. 감점 포인트(penalty) 타입은 어떻게 작동하나요?

**A**: api 호출시 `type: "penalty"`로 설정하면 학생의 포인트가 **감소**합니다.

### Q6. 일일 호출 제한을 늘릴 수 있나요?

**A**: 현재는 최대 500회로 고정되어 있습니다. 더 많은 호출이 필요한 경우 문의 게시판으로 연락주세요.

---

## 지원

### 문의

- **문의 게시판**: 그라운드 → 문의 게시판

### 버그 리포트

API 사용 중 문제가 발생하면:

1. 에러 메시지 전체

2. 요청 URL 및 본문

3. API 키 ID (키 전체가 아닌 ID만)

위 정보와 함께 문의 게시판에 제보해 주세요.

---

**마지막 업데이트**: 2025년 11월 12일
**API 버전**: v1

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