- features/ai/README.md: 토큰사용량, 단가설정, R2 비용추적 내용 추가 - frontend/api-specs/ai-token-usage-api.md: React 프론트엔드 개발자용 API 명세 신규 작성
353 lines
11 KiB
Markdown
353 lines
11 KiB
Markdown
# AI 토큰사용량 API 명세
|
|
|
|
> **작성일**: 2026-03-18
|
|
> **상태**: API 구현 완료, React 구현 대기
|
|
> **메뉴 위치**: 설정 > 토큰사용량
|
|
|
|
---
|
|
|
|
## 1. 개요
|
|
|
|
테넌트 사용자가 자기 테넌트의 AI 사용량과 비용을 조회하는 API. MNG의 AI 토큰사용량과 동일한 역할이지만, 서비스 환경에 맞게 조정되었다.
|
|
|
|
**MNG와의 차이:**
|
|
- 테넌트 필터 없음 (인증된 사용자의 테넌트로 자동 필터)
|
|
- 단가 설정은 읽기 전용 (MNG에서 관리)
|
|
- 저장소 비용은 R2 기준 (GCS 아닌)
|
|
|
|
---
|
|
|
|
## 2. API 엔드포인트
|
|
|
|
### 2.1 토큰 사용량 목록 + 통계
|
|
|
|
```
|
|
GET /api/v1/settings/ai-token-usage
|
|
```
|
|
|
|
**Query Parameters:**
|
|
|
|
| 파라미터 | 타입 | 필수 | 설명 |
|
|
|---------|------|:----:|------|
|
|
| `start_date` | string (YYYY-MM-DD) | - | 시작일 |
|
|
| `end_date` | string (YYYY-MM-DD) | - | 종료일 (start_date 이후) |
|
|
| `menu_name` | string | - | 호출 메뉴명 필터 (드롭다운 선택값) |
|
|
| `per_page` | integer | - | 페이지당 건수 (기본 20, 범위 10~100) |
|
|
| `page` | integer | - | 페이지 번호 (기본 1) |
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"items": {
|
|
"data": [
|
|
{
|
|
"id": 1,
|
|
"model": "gemini-2.0-flash",
|
|
"menu_name": "AI리포트-일간",
|
|
"prompt_tokens": 2500,
|
|
"completion_tokens": 800,
|
|
"total_tokens": 3300,
|
|
"cost_usd": "0.001320",
|
|
"cost_krw": "1.85",
|
|
"request_id": "a1b2c3d4-...",
|
|
"created_by": 5,
|
|
"creator": {
|
|
"id": 5,
|
|
"name": "홍길동"
|
|
},
|
|
"created_at": "2026-03-18T10:30:00.000000Z",
|
|
"updated_at": "2026-03-18T10:30:00.000000Z"
|
|
}
|
|
],
|
|
"current_page": 1,
|
|
"last_page": 3,
|
|
"per_page": 20,
|
|
"total": 45,
|
|
"from": 1,
|
|
"to": 20
|
|
},
|
|
"stats": {
|
|
"total_count": 45,
|
|
"total_prompt_tokens": 125000,
|
|
"total_completion_tokens": 38000,
|
|
"total_total_tokens": 163000,
|
|
"total_cost_usd": 0.0652,
|
|
"total_cost_krw": 91.28
|
|
},
|
|
"menu_names": [
|
|
"AI리포트-일간",
|
|
"AI리포트-주간",
|
|
"AI리포트-월간",
|
|
"명함OCR"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**핵심 포인트:**
|
|
- `stats`: 현재 필터 조건의 전체 집계 (페이지네이션 무관)
|
|
- `menu_names`: 해당 테넌트에서 사용한 고유 메뉴명 목록 (필터 드롭다운용)
|
|
- `creator`: 사용자 정보 (nullable — 시스템 호출 시 null)
|
|
|
|
---
|
|
|
|
### 2.2 단가 설정 조회
|
|
|
|
```
|
|
GET /api/v1/settings/ai-token-usage/pricing
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"pricing": [
|
|
{
|
|
"id": 1,
|
|
"provider": "claude",
|
|
"model_name": "claude-3-haiku",
|
|
"input_price_per_million": "0.2500",
|
|
"output_price_per_million": "1.2500",
|
|
"unit_price": null,
|
|
"unit_description": null,
|
|
"exchange_rate": "1400.00",
|
|
"is_active": true,
|
|
"description": "Claude 3 Haiku"
|
|
},
|
|
{
|
|
"id": 5,
|
|
"provider": "cloudflare-r2",
|
|
"model_name": "cloud-storage",
|
|
"input_price_per_million": "0.0000",
|
|
"output_price_per_million": "0.0000",
|
|
"unit_price": "0.004500",
|
|
"unit_description": "per 1,000,000 Class A operations",
|
|
"exchange_rate": "1400.00",
|
|
"is_active": true,
|
|
"description": "Cloudflare R2 Storage (S3 compatible)"
|
|
},
|
|
{
|
|
"id": 2,
|
|
"provider": "gemini",
|
|
"model_name": "gemini-2.0-flash",
|
|
"input_price_per_million": "0.1000",
|
|
"output_price_per_million": "0.4000",
|
|
"unit_price": null,
|
|
"unit_description": null,
|
|
"exchange_rate": "1400.00",
|
|
"is_active": true,
|
|
"description": "Gemini 2.0 Flash"
|
|
}
|
|
],
|
|
"exchange_rate": 1400.0
|
|
}
|
|
}
|
|
```
|
|
|
|
**pricing 레코드 구분:**
|
|
- `input_price_per_million` / `output_price_per_million`: AI 토큰 기반 (gemini, claude)
|
|
- `unit_price` / `unit_description`: 단위 기반 (R2 저장소, STT)
|
|
- 두 유형이 동시에 null이 아닌 경우는 없음
|
|
|
|
---
|
|
|
|
## 3. 화면 구성 가이드
|
|
|
|
### 3.1 메뉴 위치
|
|
|
|
```
|
|
설정 (Settings)
|
|
├── ...
|
|
└── 토큰사용량 (/settings/ai-token-usage)
|
|
```
|
|
|
|
### 3.2 UI 구조
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ AI 토큰 사용량 [단가 확인] │
|
|
├─────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
│ │총 호출수│ │입력토큰│ │출력토큰│ │비용 USD│ │비용 KRW│ │
|
|
│ │ 45건 │ │ 125K │ │ 38K │ │ $0.065│ │ 91원 │ │
|
|
│ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │
|
|
│ │
|
|
│ 시작일[____] 종료일[____] 메뉴[▼____] [조회][초기화]│
|
|
│ │
|
|
│ ┌────┬──────┬──────┬──────┬──────┬───────┬──────┐ │
|
|
│ │No. │ 일시 │카테고리│ 메뉴 │ 모델 │ 토큰 │ 비용 │ │
|
|
│ ├────┼──────┼──────┼──────┼──────┼───────┼──────┤ │
|
|
│ │ 1 │03-18│AI리포트│ 일간 │gemini│ 3,300│$0.001│ │
|
|
│ │ 2 │03-17│명함OCR│ 인식 │gemini│ 1,200│$0.000│ │
|
|
│ └────┴──────┴──────┴──────┴──────┴───────┴──────┘ │
|
|
│ │
|
|
│ ◀ 1 2 3 ▶ 20건 / 총 45건 │
|
|
└─────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 3.3 통계 카드 (5개)
|
|
|
|
| 카드 | 데이터 소스 | 포맷 |
|
|
|------|-----------|------|
|
|
| 총 호출 수 | `stats.total_count` | `toLocaleString()` |
|
|
| 입력 토큰 | `stats.total_prompt_tokens` | `toLocaleString()` |
|
|
| 출력 토큰 | `stats.total_completion_tokens` | `toLocaleString()` |
|
|
| 총 비용 (USD) | `stats.total_cost_usd` | `$0.0000` (소수점 4자리) |
|
|
| 총 비용 (KRW) | `stats.total_cost_krw` | `0원` (정수 반올림) |
|
|
|
|
### 3.4 테이블 컬럼
|
|
|
|
| 컬럼 | 필드 | 정렬 | 비고 |
|
|
|------|------|:----:|------|
|
|
| No. | 자동 번호 | 중앙 | |
|
|
| 사용일시 | `created_at` | 좌 | `YYYY-MM-DD HH:mm` 포맷 |
|
|
| 카테고리 | `menu_name` 기반 | 좌 | 카테고리 매핑 함수 적용 |
|
|
| 호출메뉴 | `menu_name` | 좌 | |
|
|
| 모델 | `model` | 좌 | |
|
|
| 입력토큰 | `prompt_tokens` | 우 | 천단위 콤마 |
|
|
| 출력토큰 | `completion_tokens` | 우 | 천단위 콤마 |
|
|
| 전체토큰 | `total_tokens` | 우 | 천단위 콤마 |
|
|
| 비용(USD) | `cost_usd` | 우 | `$0.0000` |
|
|
| 비용(KRW) | `cost_krw` | 우 | `0원` |
|
|
| 사용자 | `creator.name` | 좌 | null이면 `-` 표시 |
|
|
|
|
### 3.5 카테고리 매핑
|
|
|
|
`menu_name` 필드를 기반으로 카테고리를 분류한다:
|
|
|
|
```typescript
|
|
function getCategory(menuName: string): string {
|
|
if (!menuName) return '-';
|
|
if (menuName.startsWith('AI리포트')) return 'AI리포트';
|
|
if (menuName.includes('명함')) return '명함OCR';
|
|
if (menuName.includes('사업자등록증')) return 'OCR';
|
|
if (menuName.startsWith('파일업로드')) return '파일저장소';
|
|
return menuName;
|
|
}
|
|
```
|
|
|
|
### 3.6 필터
|
|
|
|
- **서버 사이드 필터링** (클라이언트 필터링 아님)
|
|
- 시작일 / 종료일: date picker
|
|
- 메뉴: `menu_names` 배열로 드롭다운 구성
|
|
- 조회 버튼 클릭 시 API 재호출
|
|
- 초기화 버튼: 모든 필터 초기화 후 재호출
|
|
|
|
### 3.7 단가 확인 모달 (읽기 전용)
|
|
|
|
`[단가 확인]` 버튼 클릭 시 모달 표시. `GET /pricing` API 호출.
|
|
|
|
**구성:**
|
|
- AI 토큰 단가 테이블 (provider, model, 입력단가, 출력단가)
|
|
- 저장소/서비스 단가 테이블 (provider, 단위가격, 단위설명)
|
|
- 현재 환율 표시
|
|
- "단가 변경은 관리자에게 문의하세요" 안내 문구
|
|
|
|
---
|
|
|
|
## 4. TypeScript 타입 정의
|
|
|
|
```typescript
|
|
interface AiTokenUsageItem {
|
|
id: number;
|
|
model: string;
|
|
menu_name: string;
|
|
prompt_tokens: number;
|
|
completion_tokens: number;
|
|
total_tokens: number;
|
|
cost_usd: string;
|
|
cost_krw: string;
|
|
request_id: string;
|
|
created_by: number | null;
|
|
creator: { id: number; name: string } | null;
|
|
created_at: string;
|
|
}
|
|
|
|
interface AiTokenUsageStats {
|
|
total_count: number;
|
|
total_prompt_tokens: number;
|
|
total_completion_tokens: number;
|
|
total_total_tokens: number;
|
|
total_cost_usd: number;
|
|
total_cost_krw: number;
|
|
}
|
|
|
|
interface AiTokenUsageListResponse {
|
|
items: PaginatedResponse<AiTokenUsageItem>;
|
|
stats: AiTokenUsageStats;
|
|
menu_names: string[];
|
|
}
|
|
|
|
interface AiPricingItem {
|
|
id: number;
|
|
provider: string;
|
|
model_name: string;
|
|
input_price_per_million: string | null;
|
|
output_price_per_million: string | null;
|
|
unit_price: string | null;
|
|
unit_description: string | null;
|
|
exchange_rate: string;
|
|
is_active: boolean;
|
|
description: string | null;
|
|
}
|
|
|
|
interface AiPricingResponse {
|
|
pricing: AiPricingItem[];
|
|
exchange_rate: number;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 컴포넌트 구조
|
|
|
|
```
|
|
src/app/[locale]/(protected)/settings/ai-token-usage/
|
|
└── page.tsx
|
|
|
|
src/components/settings/AiTokenUsage/
|
|
├── index.tsx ← 메인 (UniversalListPage 패턴)
|
|
├── actions.ts ← Server Actions
|
|
├── types.ts ← TypeScript 타입
|
|
├── AiTokenUsageConfig.ts ← 카테고리 매핑, 포맷 유틸
|
|
└── PricingModal.tsx ← 단가 조회 모달 (읽기 전용)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Server Actions
|
|
|
|
```typescript
|
|
// actions.ts
|
|
'use server';
|
|
|
|
export async function getAiTokenUsageList(params?: {
|
|
start_date?: string;
|
|
end_date?: string;
|
|
menu_name?: string;
|
|
per_page?: number;
|
|
page?: number;
|
|
}): Promise<ActionResult<AiTokenUsageListResponse>>
|
|
|
|
export async function getAiTokenUsagePricing():
|
|
Promise<ActionResult<AiPricingResponse>>
|
|
```
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
- [features/ai/README.md](../../features/ai/README.md) — AI 기능 전체 개요
|
|
- [이관 기획서](../../plans/ai-token-usage-service-migration.md) — MNG→서비스 이관 상세 기획
|
|
|
|
---
|
|
|
|
**최종 업데이트**: 2026-03-18
|