# 🥦 그라운드 학생 정보 조회 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%95%99%EC%83%9D-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-api)

5. [응답 데이터 상세](https://#%EC%9D%91%EB%8B%B5-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%81%EC%84%B8)

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

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

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

---

## 시작하기

그라운드 학생 정보 조회 API를 사용하면 외부 애플리케이션에서 학생의 상세 정보를 조회할 수 있습니다. 포인트, 레벨, 드래곤 정보, 보유 아이템 등을 단일 API 호출로 가져올 수 있습니다.

### 기본 정보

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

- **프로토콜**: HTTPS

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

- **필요 권한**: `readStudents`

### 조회 가능한 정보

- ✅ 포인트 및 레벨 정보

- ✅ 드래곤 정보 (이름, 단계, 레벨, 경험치, 먹이, 하트스톤)

- ✅ 보유 마켓 상품 (일반 마켓 + 공동구매)

- ✅ 텃밭 아이템 (배치 아이템, 인벤토리, 하트베리)

---

## API 키 발급

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

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

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

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

### 3. API 키 관리 탭

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

### 4. 새 API 키 생성

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

2. 다음 정보 입력:

- **API 키 이름**: 애플리케이션 이름 (예: "대시보드 앱")

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

- **권한 설정**: 

    - ✅ **학생 정보 조회 권한** (`readStudents`) - 필수

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

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

### 5. API 키 저장

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

```
sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p4
```

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

### 제한 사항

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

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

---

## 인증 방식

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

```
X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p4
```

---

## 학생 정보 조회 API

### 엔드포인트

```
GET /api/v1/classes/{classId}/students/{studentCode}
```

### URL 파라미터

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

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

### 요청 헤더

| 헤더 | 값 | 필수 |
| --- | --- | --- |
| `X-API-Key` | 발급받은 API 키 | ✅ |

> ⚠️ **권한 필요**: API 키에 `readStudents` 권한이 있어야 합니다.

### 요청 예시

```
curl -X GET \
  "https://growndcard.com/api/v1/classes/NP0hetJ3wyQKFtRnFeftmPiy8Dl4_2/students/2" \
  -H "X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p4"
```

### 성공 응답 (200 OK)

```
{
  "success": true,
  "data": {
    "studentId": "stu_abc123",
    "studentCode": 2,
    "studentName": "홍길동",
    "avatar": "/avatars/a1.png",
    "points": {
      "totalPoints": 150,
      "rewardPoints": 180,
      "penaltyPoints": 30,
      "currentLevel": 4,
      "levelName": "아스파라거스"
    },
    "dragon": {
      "name": "불꽃이",
      "stage": "baby",
      "level": 3,
      "absoluteLevel": 8,
      "experience": 45,
      "food": 12,
      "color": "red",
      "heartstones": 5
    },
    "marketInventory": {
      "totalItems": 12,
      "regularMarket": {
        "totalCount": 5,
        "products": [
          {
            "productName": "연필 세트",
            "productEmoji": "✏️"
          },
          {
            "productName": "지우개",
            "productEmoji": "🧹"
          }
        ]
      },
      "commonMarket": {
        "totalCount": 4,
        "products": [
          {
            "productName": "간식 세트",
            "productEmoji": "🍪"
          }
        ]
      },
      "cave": {
        "totalCount": 3,
        "products": [
          {
            "productName": "드래곤 먹이",
            "productEmoji": "🥩"
          }
        ]
      }
    },
    "farmItems": {
      "heartBerries": 25,
      "placedItems": [
        {
          "itemId": "item_001",
          "name": "분수",
          "emoji": "⛲",
          "x": 2,
          "y": 3
        },
        {
          "itemId": "item_002",
          "name": "나무",
          "emoji": "🌳",
          "x": 4,
          "y": 2
        }
      ],
      "inventoryItems": [
        {
          "itemId": "item_003",
          "name": "꽃",
          "emoji": "🌸",
          "quantity": 5
        },
        {
          "itemId": "item_004",
          "name": "벤치",
          "emoji": "🪑",
          "quantity": 2
        }
      ],
      "farmBackground": "spring",
      "placedItemBonusPoints": 2.5
    }
  },
  "message": "학생 정보 조회에 성공했습니다."
}
```

---

## 응답 데이터 상세

### 학생 기본 정보

| 필드 | 타입 | 설명 | 예시 |
| --- | --- | --- | --- |
| `studentId` | string | 학생의 내부 ID | `"stu_abc123"` |
| `studentCode` | number | 학생 번호 (출석번호) | `2` |
| `studentName` | string | 학생 이름 | `"홍길동"` |
| `avatar` | string | 아바타 이미지 경로 | `"/avatars/a1.png"` |

### 포인트 정보 (`points`)

| 필드 | 타입 | 설명 | 범위 |
| --- | --- | --- | --- |
| `totalPoints` | number | 총 포인트 (리워드 - 페널티) | - |
| `rewardPoints` | number | 누적 리워드 포인트 | >= 0 |
| `penaltyPoints` | number | 누적 페널티 포인트 | >= 0 |
| `currentLevel` | number | 현재 레벨 | 1-50 |
| `levelName` | string | 레벨 명칭 | "아몬드" 등 (식물 이름) |

### 드래곤 정보 (`dragon`)

| 필드 | 타입 | 설명 | 값 |
| --- | --- | --- | --- |
| `name` | string | 드래곤 이름 (베이비 단계부터 설정 가능) | `"불꽃이"` |
| `stage` | string | 드래곤 단계 | `egg`, `baby`, `junior`, `strong`, `super` |
| `level` | number | 단계별 레벨 | 1-5 |
| `absoluteLevel` | number | 절대 레벨 (전체 진행도) | 0-25 |
| `experience` | number | 현재 경험치 | >= 0 |
| `food` | number | 보유 먹이 개수 | >= 0 |
| `color` | string | 드래곤 색상 | `red`, `blue`, `green` 등 |
| `heartstones` | number | 보유 하트스톤 개수 | >= 0 |

> 💡 드래곤 정보가 없으면 `null`이 반환됩니다 (아직 초기화되지 않은 경우).

### 마켓 인벤토리 (`marketInventory`)

학생의 마켓 상품 정보입니다.

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `totalItems` | number | 전체 구매 기록 개수 (3가지 마켓 타입의 총합) |
| `regularMarket` | object | 일반 마켓 상품 정보 |
| `commonMarket` | object | 공동구매 상품 정보 |
| `cave` | object | 케이브 (드래곤 동굴) 상품 정보 |

### 각 마켓 타입 구조 (`regularMarket`, `commonMarket`, `cave`)

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `totalCount` | number | 해당 마켓 타입의 상품 개수 |
| `products` | array | 보유 중인 상품 목록 (중복 제거됨) |

### 상품 객체 (`products` 배열의 각 항목)

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `productName` | string | 상품 이름 |
| `productEmoji` | string | 상품 이모지 (선택) |

> 💡 **totalItems vs totalCount**

- `totalItems`: **전체 보유 상품의 총 개수** (일반 마켓 + 공동구매 + 케이브 합계)

- `totalCount`: **각 마켓 타입별 보유 상품 개수**

- 예: 연필 3개, 지우개 2개 보유 → totalCount = 5개

- 예: 일반 마켓 5개 + 공동구매 4개 + 케이브 3개 → totalItems = 12개

> 📝 **products 배열 특징**

- **현재 보유 중인 상품의 종류만 표시** (같은 상품을 여러 개 보유해도 1개만 표시)

- 개별 상품의 보유 개수는 표시하지 않음

- "어떤 종류의 상품을 보유하고 있는가"를 보여주는 용도

- 예: 연필 3개 보유 → products 배열에 "연필" 1개만 표시

> ⚠️ **중요: 실시간 보유 수량**

- `totalCount`와 `totalItems`는 **현재 실제 보유 중인 해당 카테고리 및 총 상품 개수**입니다

- 실시간으로 학생의 현재 상품 보유 상태를 정확히 반영합니다

### 텃밭 아이템 (`farmItems`)

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `heartBerries` | number | 보유 하트베리 개수 |
| `placedItems` | array | 텃밭에 배치된 아이템 목록 |
| `inventoryItems` | array | 인벤토리 아이템 목록 (수량 포함) |
| `farmBackground` | string | 텃밭 배경 |
| `placedItemBonusPoints` | number | 배치 아이템의 포인트 보너스 합계 |

**placedItems 구조**

```
{
  itemId: string;      // 아이템 ID
  name: string;        // 아이템 이름
  emoji: string;       // 아이템 이모지
  x: number;           // X 좌표
  y: number;           // Y 좌표
}
```

**inventoryItems 구조**

```
{
  itemId: string;      // 아이템 ID
  name: string;        // 아이템 이름
  emoji: string;       // 아이템 이모지
  quantity: number;    // 보유 수량
}
```

---

## 에러 처리

### 에러 응답 형식

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

### 에러 코드

| 상태 코드 | 에러 코드 | 설명 | 해결 방법 |
| --- | --- | --- | --- |
| 400 | `invalid_student_code` | 학생 번호가 제공되지 않음 | studentCode 확인 |
| 401 | `unauthorized` | API 키가 없거나 유효하지 않음 | API 키 확인, X-API-Key 헤더 확인 |
| 403 | `forbidden` | 해당 클래스 접근 권한 없음 | classId 확인 |
| 403 | `insufficient_permissions` | 학생 조회 권한이 없음 | API 키의 `readStudents` 권한 확인 |
| 404 | `student_not_found` | 학생을 찾을 수 없음 | studentCode 확인 |
| 429 | `rate_limit_exceeded` | 호출 제한 초과 | 잠시 후 재시도 |
| 500 | `internal_error` | 서버 내부 오류 | 잠시 후 재시도, 반복되면 문의 |

### 에러 응답 예시

```
{
  "success": false,
  "error": {
    "code": "insufficient_permissions",
    "message": "학생 정보 조회 권한이 없습니다.",
    "details": {
      "requiredPermission": "readStudents"
    }
  }
}
```

---

## 코드 예제

### cURL

```
curl -X GET \
  "https://growndcard.com/api/v1/classes/NP0hetJ3wyQKFtRnFeftmPiy8Dl4_2/students/2" \
  -H "X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p4"
```

### JavaScript (Node.js)

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

const API_KEY = 'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p4';
const BASE_URL = 'https://growndcard.com';
const CLASS_ID = 'NP0hetJ3wyQKFtRnFeftmPiy8Dl4_2';

async function getStudentInfo(studentCode) {
  try {
    const response = await fetch(
      `${BASE_URL}/api/v1/classes/${CLASS_ID}/students/${studentCode}`,
      {
        method: 'GET',
        headers: {
          'X-API-Key': API_KEY
        }
      }
    );

    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;
  }
}

// 사용 예시
getStudentInfo(2)
  .then(student => {
    console.log(`학생: ${student.studentName}`);
    console.log(`총 포인트: ${student.points.totalPoints}`);
    console.log(`레벨: ${student.points.currentLevel} (${student.points.levelName})`);
    
    if (student.dragon) {
      console.log(`드래곤: ${student.dragon.name || '이름 없음'} (Lv.${student.dragon.absoluteLevel})`);
    }
    
    console.log(`보유 상품 수: ${student.marketProducts.length}개`);
    console.log(`하트베리: ${student.farmItems.heartBerries}개`);
  })
  .catch(error => console.error('에러:', error));
```

### Python

```
import requests
import json

API_KEY = 'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p4'
BASE_URL = 'https://growndcard.com'
CLASS_ID = 'NP0hetJ3wyQKFtRnFeftmPiy8Dl4_2'

def get_student_info(student_code):
    """학생 정보 조회"""
    url = f'{BASE_URL}/api/v1/classes/{CLASS_ID}/students/{student_code}'
    
    headers = {
        'X-API-Key': API_KEY
    }
    
    try:
        response = requests.get(url, headers=headers)
        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__':
    student = get_student_info(2)
    
    print(f"학생: {student['studentName']}")
    print(f"총 포인트: {student['points']['totalPoints']}")
    print(f"레벨: {student['points']['currentLevel']} ({student['points']['levelName']})")
    
    if student['dragon']:
        dragon_name = student['dragon'].get('name', '이름 없음')
        print(f"드래곤: {dragon_name} (Lv.{student['dragon']['absoluteLevel']})")
    
    print(f"전체 마켓 상품: {student['marketInventory']['totalItems']}개")
    print(f"일반 마켓: {student['marketInventory']['regularMarket']['totalCount']}개")
    print(f"하트베리: {student['farmItems']['heartBerries']}개")
```

### TypeScript (React/Next.js)

```
interface StudentInfoResponse {
  success: boolean;
  data?: {
    studentId: string;
    studentCode: number;
    studentName: string;
    avatar?: string;
    points: {
      totalPoints: number;
      rewardPoints: number;
      penaltyPoints: number;
      currentLevel: number;
      levelName: string;
    };
    dragon: {
      name?: string;
      stage: string;
      level: number;
      absoluteLevel: number;
      experience: number;
      food: number;
      color?: string;
      heartstones: number;
    } | null;
    marketInventory: {
      totalItems: number;
      regularMarket: {
        totalCount: number;
        products: Array<{
          productName: string;
          productEmoji?: string;
        }>;
      };
      commonMarket: {
        totalCount: number;
        products: Array<{
          productName: string;
          productEmoji?: string;
        }>;
      };
      cave: {
        totalCount: number;
        products: Array<{
          productName: string;
          productEmoji?: string;
        }>;
      };
    };
    farmItems: {
      heartBerries: number;
      placedItems: Array<{
        itemId: string;
        name: string;
        emoji: string;
        x: number;
        y: number;
      }>;
      inventoryItems: Array<{
        itemId: string;
        name: string;
        emoji: string;
        quantity?: number;
      }>;
      farmBackground?: string;
      placedItemBonusPoints?: number;
    };
  };
  message?: string;
}

class GroundStudentApiClient {
  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 getStudentInfo(studentCode: number): Promise<StudentInfoResponse> {
    const url = `${this.baseUrl}/api/v1/classes/${this.classId}/students/${studentCode}`;

    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'X-API-Key': this.apiKey
      }
    });

    const data = await response.json();

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

    return data;
  }
}

// 사용 예시
const client = new GroundStudentApiClient(
  'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
  'NP0hetJ3wyQKFtRnFeftmPiy8Dl2_2'
);

async function example() {
  try {
    const result = await client.getStudentInfo(2);
    const student = result.data;
    
    if (!student) return;

    console.log(`학생: ${student.studentName}`);
    console.log(`총 포인트: ${student.points.totalPoints}`);
    console.log(`레벨: ${student.points.currentLevel} (${student.points.levelName})`);
    
    if (student.dragon) {
      console.log(`드래곤: ${student.dragon.name || '이름 없음'} (Lv.${student.dragon.absoluteLevel})`);
      console.log(`먹이: ${student.dragon.food}개, 경험치: ${student.dragon.experience}`);
    }
    
    console.log(`전체 마켓 상품: ${student.marketInventory.totalItems}개`);
    console.log(`일반 마켓: ${student.marketInventory.regularMarket.totalCount}개`);
    console.log(`공동구매: ${student.marketInventory.commonMarket.totalCount}개`);
    console.log(`케이브: ${student.marketInventory.cave.totalCount}개`);
    
    console.log(`하트베리: ${student.farmItems.heartBerries}개`);
    console.log(`배치 아이템: ${student.farmItems.placedItems.length}개`);
    console.log(`인벤토리 아이템: ${student.farmItems.inventoryItems.length}종류`);
  } catch (error) {
    console.error('학생 정보 조회 실패:', error);
  }
}
```

### PHP

```
<?php

function getStudentInfo($studentCode) {
    $apiKey = 'sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p4';
    $classId = 'NP0hetJ3wyQKFtRnFeftmPiy8Dl4_2';
    $baseUrl = 'https://growndcard.com';
    
    $url = "{$baseUrl}/api/v1/classes/{$classId}/students/{$studentCode}";
    
    $options = [
        'http' => [
            'method' => 'GET',
            'header' => [
                "X-API-Key: {$apiKey}"
            ]
        ]
    ];
    
    $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 {
    $student = getStudentInfo(2);
    
    echo "학생: {$student['studentName']}\n";
    echo "총 포인트: {$student['points']['totalPoints']}\n";
    echo "레벨: {$student['points']['currentLevel']} ({$student['points']['levelName']})\n";
    
    if ($student['dragon']) {
        $dragonName = $student['dragon']['name'] ?? '이름 없음';
        echo "드래곤: {$dragonName} (Lv.{$student['dragon']['absoluteLevel']})\n";
    }
    
    echo "전체 마켓 상품: {$student['marketInventory']['totalItems']}개\n";
    echo "일반 마켓: {$student['marketInventory']['regularMarket']['totalCount']}개\n";
    echo "하트베리: {$student['farmItems']['heartBerries']}개\n";
} catch (Exception $e) {
    echo "에러: " . $e->getMessage() . "\n";
}
```

---

## 제한 사항

### API 키 제한

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

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

### 호출 제한 (Rate Limit)

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

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

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

### 데이터 제한

- 마켓 상품: 실제 구매한 상품만 조회 가능

- 텃밭 아이템: 현재 보유 중인 아이템만 조회 가능

- 드래곤: 초기화되지 않은 경우 `null` 반환

### 보안 권장사항

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

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

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

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

5. ✅ 학생 개인정보 보호에 유의하세요

---

## FAQ

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

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

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

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

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

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

### Q4. 여러 학생의 정보를 한 번에 조회할 수 있나요?

**A**: 현재는 단일 학생만 조회 가능합니다. 여러 학생을 조회하려면 각 학생마다 API를 호출해야 합니다.

### Q5. 드래곤 정보가 null로 나옵니다.

**A**: 학생이 아직 드래곤을 초기화하지 않은 경우 `null`이 반환됩니다. 학생이 그라운드에서 드래곤 시스템을 활성화하면 정보가 조회됩니다.

### Q6. 조회 권한만 있으면 포인트 부여는 못하나요?

**A**: 네, `readStudents` 권한만으로는 포인트 부여가 불가능합니다. 포인트 부여가 필요하면 `awardPoints` 권한도 함께 활성화하세요.

### Q7. 학생의 과거 포인트 이력도 조회할 수 있나요?

**A**: 현재는 누적 포인트와 현재 상태만 조회 가능합니다. 포인트 부여 이력은 그라운드 웹사이트에서만 확인할 수 있습니다.

---

## 활용 예시

### 대시보드 앱

학생들의 포인트와 레벨을 실시간으로 모니터링하는 대시보드를 만들 수 있습니다.

### 리더보드 시스템

여러 학생의 정보를 조회하여 순위를 매기는 리더보드를 구현할 수 있습니다.

### 분석 도구

학생들의 포인트 분포, 드래곤 레벨 통계 등을 분석하는 도구를 만들 수 있습니다.

### 학부모 앱

학생 코드로 자녀의 학습 진행 상황을 조회하는 앱을 만들 수 있습니다.

---

## 지원

### 문의

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

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