diff --git a/INDEX.md b/INDEX.md index 02e9e3a..19f7060 100644 --- a/INDEX.md +++ b/INDEX.md @@ -290,6 +290,7 @@ DB 도메인별: | [receiving-lot-auto-generate-request.md](plans/receiving-lot-auto-generate-request.md) | 입고등록 원자재로트번호 자동채번 적용 요청 (읽기전용 변경, API 자동생성) | | [stock-detail-inventory-adjustment-request.md](plans/stock-detail-inventory-adjustment-request.md) | 재고 조정 위치 이동 요청 (입고관리 → 재고 상세 화면) | | [bom-tree-visualization-request.md](plans/bom-tree-visualization-request.md) | BOM Tree 시각화 React 구현 요청 (API 완료, 재귀 트리 UI) | +| [ai-token-usage-service-migration.md](plans/ai-token-usage-service-migration.md) | AI 토큰사용량 서비스 이관 기획 (MNG→API+React, GCS→R2 저장소 차이) | ### frontend/integration/ — 프론트엔드 개발 가이드 diff --git a/plans/ai-token-usage-service-migration.md b/plans/ai-token-usage-service-migration.md new file mode 100644 index 0000000..df016de --- /dev/null +++ b/plans/ai-token-usage-service-migration.md @@ -0,0 +1,681 @@ +# 서비스 AI 토큰사용량 기능 기획 + +> **작성일**: 2026-03-18 +> **상태**: 기획 +> **대상**: sam/api + sam/react +> **원본**: sam/mng `system/ai-token-usage` 메뉴 + +--- + +## 1. 개요 + +### 1.1 목적 + +MNG(백오피스)의 AI 토큰사용량 조회 기능을 서비스(API+React)의 **설정 > 토큰사용량** 메뉴로 이관한다. 테넌트 사용자가 자기 테넌트의 AI 사용량과 비용을 직접 확인할 수 있도록 한다. + +### 1.2 핵심 차이점 (MNG vs 서비스) + +| 항목 | MNG (백오피스) | 서비스 (API+React) | +|------|---------------|-------------------| +| **사용자** | 시스템 관리자 | 테넌트 사용자 | +| **테넌트 필터** | 전체 테넌트 조회 가능 | 자기 테넌트만 조회 (자동 필터) | +| **저장소 비용** | GCS (Google Cloud Storage) | R2 (Cloudflare R2) | +| **단가 관리** | 모달에서 직접 편집 | 조회만 (단가 변경은 MNG에서) | +| **UI** | Blade + React (HTMX) | Next.js (SSR) | +| **인증** | 세션 기반 | Sanctum 토큰 기반 | + +### 1.3 저장소 정책 차이 + +> MNG는 음성녹음/회의록에 **GCS(Google Cloud Storage)**를 사용하지만, +> 서비스(API)는 파일 저장에 **R2(Cloudflare R2)**를 사용한다. + +| 항목 | MNG (GCS) | 서비스 (R2) | +|------|-----------|------------| +| **저장소** | Google Cloud Storage | Cloudflare R2 (S3 호환) | +| **과금 단위** | Class A $0.005/1000건 + $0.02/GB/월 | Class A $0.0045/M건 + $0.015/GB/월 | +| **비용 추적 메서드** | `saveGcsStorageUsage()` | `saveR2StorageUsage()` (신규) | +| **pricing provider** | `google-gcs` | `cloudflare-r2` (신규) | +| **STT** | Google Speech-to-Text | 서비스에서 STT 미사용 (해당 없음) | + +--- + +## 2. sam/api 기획 + +### 2.1 API 엔드포인트 + +``` +GET /api/v1/settings/ai-token-usage # 토큰 사용량 목록 + 통계 +GET /api/v1/settings/ai-token-usage/pricing # 단가 설정 조회 +``` + +> 단가 수정은 MNG 전용이므로 서비스 API에는 PUT 엔드포인트를 두지 않는다. + +### 2.2 라우트 등록 + +```php +// routes/api/v1/common.php — Settings 그룹 내부에 추가 +Route::prefix('settings')->group(function () { + // ... 기존 설정 라우트 ... + + // AI 토큰 사용량 + Route::get('/ai-token-usage', [AiTokenUsageController::class, 'index']) + ->name('v1.settings.ai-token-usage.index'); + Route::get('/ai-token-usage/pricing', [AiTokenUsageController::class, 'pricing']) + ->name('v1.settings.ai-token-usage.pricing'); +}); +``` + +### 2.3 컨트롤러 + +**파일**: `app/Http/Controllers/Api/V1/AiTokenUsageController.php` + +```php +class AiTokenUsageController extends Controller +{ + public function __construct( + private AiTokenUsageService $service + ) {} + + // GET /api/v1/settings/ai-token-usage + public function index(AiTokenUsageListRequest $request) + { + return ApiResponse::handle(fn () => + $this->service->list($request->validated()) + ); + } + + // GET /api/v1/settings/ai-token-usage/pricing + public function pricing() + { + return ApiResponse::handle(fn () => + $this->service->getPricing() + ); + } +} +``` + +### 2.4 서비스 + +**파일**: `app/Services/AiTokenUsageService.php` + +```php +class AiTokenUsageService extends Service +{ + /** + * 토큰 사용량 목록 + 통계 + * - tenant_id는 Service 부모 클래스의 tenantId()로 자동 적용 + * - 테넌트 필터 없음 (자기 테넌트만) + */ + public function list(array $params): array + { + // 1. 사용량 목록 (페이지네이션) + $query = AiTokenUsage::where('tenant_id', $this->tenantId()) + ->when($params['start_date'] ?? null, fn ($q, $d) => $q->where('created_at', '>=', $d)) + ->when($params['end_date'] ?? null, fn ($q, $d) => $q->where('created_at', '<=', $d . ' 23:59:59')) + ->when($params['menu_name'] ?? null, fn ($q, $m) => $q->where('menu_name', $m)) + ->with('creator:id,name') + ->orderByDesc('created_at'); + + $items = $query->paginate($params['per_page'] ?? 20); + + // 2. 통계 (동일 필터 조건, 전체 집계) + $statsQuery = AiTokenUsage::where('tenant_id', $this->tenantId()) + ->when($params['start_date'] ?? null, fn ($q, $d) => $q->where('created_at', '>=', $d)) + ->when($params['end_date'] ?? null, fn ($q, $d) => $q->where('created_at', '<=', $d . ' 23:59:59')) + ->when($params['menu_name'] ?? null, fn ($q, $m) => $q->where('menu_name', $m)); + + $stats = [ + 'total_count' => $statsQuery->count(), + 'total_prompt_tokens' => (int) $statsQuery->sum('prompt_tokens'), + 'total_completion_tokens' => (int) $statsQuery->sum('completion_tokens'), + 'total_total_tokens' => (int) $statsQuery->sum('total_tokens'), + 'total_cost_usd' => (float) $statsQuery->sum('cost_usd'), + 'total_cost_krw' => (float) $statsQuery->sum('cost_krw'), + ]; + + // 3. 메뉴명 필터 옵션 (해당 테넌트의 고유 menu_name 목록) + $menuNames = AiTokenUsage::where('tenant_id', $this->tenantId()) + ->select('menu_name') + ->distinct() + ->orderBy('menu_name') + ->pluck('menu_name'); + + return [ + 'items' => $items, + 'stats' => $stats, + 'menu_names' => $menuNames, + ]; + } + + /** + * 단가 설정 조회 (읽기 전용) + */ + public function getPricing(): array + { + $configs = AiPricingConfig::where('is_active', true) + ->orderBy('provider') + ->get(); + + return [ + 'pricing' => $configs, + 'exchange_rate' => AiPricingConfig::getExchangeRate(), + ]; + } +} +``` + +### 2.5 FormRequest + +**파일**: `app/Http/Requests/V1/AiTokenUsageListRequest.php` + +```php +class AiTokenUsageListRequest extends FormRequest +{ + public function rules(): array + { + return [ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'menu_name' => 'nullable|string|max:100', + 'per_page' => 'nullable|integer|min:10|max:100', + 'page' => 'nullable|integer|min:1', + ]; + } +} +``` + +### 2.6 모델 (기존 활용) + +> `ai_token_usages`, `ai_pricing_configs` 테이블은 이미 API에서 마이그레이션 관리 중이다. +> 모델도 `app/Models/Tenants/AiTokenUsage.php`, `AiPricingConfig.php`에 존재한다. + +**추가 필요 없음** — 기존 모델을 그대로 사용한다. + +### 2.7 AiTokenHelper 이관 + +> MNG의 `AiTokenHelper`를 API에도 동일하게 구현하되, GCS 대신 R2 저장소 비용 추적 메서드를 추가한다. + +**파일**: `app/Helpers/AiTokenHelper.php` + +기존 MNG와 동일한 메서드 + R2 전용 메서드: + +| 메서드 | 출처 | 서비스 적용 | +|--------|------|-----------| +| `saveGeminiUsage()` | MNG 동일 | AI 리포트 등 Gemini 호출 시 | +| `saveClaudeUsage()` | MNG 동일 | Claude 호출 시 | +| `saveR2StorageUsage()` | 신규 | R2 파일 업로드 시 | +| `saveSttUsage()` | MNG 동일 | 서비스에서 STT 사용 시 (현재 미사용) | + +**R2 저장소 비용 계산 공식**: + +```php +public static function saveR2StorageUsage(string $menuName, int $fileSizeBytes): void +{ + // Cloudflare R2 요금 (2026년 기준) + // Class A (PUT/POST): $0.0045 / 1,000,000건 → 건당 $0.0000000045 + // Storage: $0.015 / GB / 월 + $pricing = AiPricingConfig::getActivePricing('cloudflare-r2'); + $unitPrice = $pricing ? (float) $pricing->unit_price : 0.0045; + + $operationCost = $unitPrice / 1_000_000; // 1건당 오퍼레이션 비용 + $fileSizeGB = $fileSizeBytes / (1024 * 1024 * 1024); + $storageCost = $fileSizeGB * 0.015 * (30 / 30); // 월간 보관 기준 + + $costUsd = $operationCost + $storageCost; + + self::save('cloudflare-r2', $menuName, $fileSizeBytes, 0, $fileSizeBytes, + $costUsd / max($fileSizeBytes, 1), 0); +} +``` + +### 2.8 AiPricingConfig 시드 데이터 추가 + +> `cloudflare-r2` provider를 `ai_pricing_configs` 테이블에 추가한다. + +```sql +INSERT INTO ai_pricing_configs +(provider, model_name, input_price_per_million, output_price_per_million, + unit_price, unit_description, exchange_rate, is_active, description) +VALUES +('cloudflare-r2', 'cloud-storage', 0, 0, + 0.0045, 'per 1,000,000 Class A operations', 1400, 1, + 'Cloudflare R2 Storage (S3 compatible)'); +``` + +> tinker 또는 마이그레이션으로 추가. 시더 실행 금지 원칙에 따라 tinker 사용 권장. + +### 2.9 API 응답 형식 + +#### `GET /api/v1/settings/ai-token-usage` + +```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": "uuid-...", + "created_by": 5, + "creator": { "id": 5, "name": "홍길동" }, + "created_at": "2026-03-18T10:30:00" + } + ], + "current_page": 1, + "last_page": 3, + "per_page": 20, + "total": 45 + }, + "stats": { + "total_count": 45, + "total_prompt_tokens": 125000, + "total_completion_tokens": 38000, + "total_total_tokens": 163000, + "total_cost_usd": 0.065200, + "total_cost_krw": 91.28 + }, + "menu_names": [ + "AI리포트-일간", + "AI리포트-주간", + "AI리포트-월간", + "명함OCR" + ] + } +} +``` + +#### `GET /api/v1/settings/ai-token-usage/pricing` + +```json +{ + "success": true, + "data": { + "pricing": [ + { + "provider": "gemini", + "model_name": "gemini-2.0-flash", + "input_price_per_million": "0.1000", + "output_price_per_million": "0.4000", + "unit_price": null, + "exchange_rate": "1400.00", + "is_active": true + }, + { + "provider": "claude", + "model_name": "claude-3-haiku", + "input_price_per_million": "0.2500", + "output_price_per_million": "1.2500", + "unit_price": null, + "exchange_rate": "1400.00", + "is_active": true + }, + { + "provider": "cloudflare-r2", + "model_name": "cloud-storage", + "input_price_per_million": null, + "output_price_per_million": null, + "unit_price": "0.004500", + "unit_description": "per 1,000,000 Class A operations", + "exchange_rate": "1400.00", + "is_active": true + } + ], + "exchange_rate": 1400.0 + } +} +``` + +### 2.10 파일 목록 (sam/api) + +| 상태 | 파일 | 설명 | +|------|------|------| +| 신규 | `app/Http/Controllers/Api/V1/AiTokenUsageController.php` | 컨트롤러 | +| 신규 | `app/Services/AiTokenUsageService.php` | 서비스 | +| 신규 | `app/Http/Requests/V1/AiTokenUsageListRequest.php` | FormRequest | +| 신규 | `app/Helpers/AiTokenHelper.php` | 토큰 사용량 기록 헬퍼 | +| 수정 | `routes/api/v1/common.php` | 라우트 추가 | +| 기존 | `app/Models/Tenants/AiTokenUsage.php` | 모델 (수정 불필요) | +| 기존 | `app/Models/Tenants/AiPricingConfig.php` | 모델 (수정 불필요) | + +--- + +## 3. sam/react 기획 + +### 3.1 메뉴 위치 + +``` +설정 (Settings) +├── 계좌관리 +├── 권한관리 +├── 직급관리 +├── 직위관리 +├── ... +└── 토큰사용량 ← 신규 추가 +``` + +### 3.2 페이지 구조 + +**라우트**: `/settings/ai-token-usage` + +``` +/settings/ai-token-usage/page.tsx → AiTokenUsagePage 컴포넌트 호출 +/components/settings/AiTokenUsage/ +├── index.tsx → 메인 컴포넌트 +├── actions.ts → Server Actions (API 호출) +├── types.ts → TypeScript 타입 정의 +├── AiTokenUsageConfig.ts → 상수, 카테고리 매핑 +└── PricingModal.tsx → 단가 조회 모달 +``` + +### 3.3 화면 구성 + +``` +┌─────────────────────────────────────────────────────────┐ +│ AI 토큰 사용량 [단가 확인] │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ 호출수│ │ 입력 │ │ 출력 │ │ 비용 │ │ 비용 │ │ +│ │ 45 │ │ 125K │ │ 38K │ │$0.065│ │ 91원 │ │ +│ │ │ │토큰 │ │토큰 │ │(USD) │ │(KRW) │ │ +│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ +│ │ +│ 시작일 [____] 종료일 [____] 메뉴 [▼____] [조회][초기화]│ +│ │ +│ ┌─────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ +│ │ 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.4 컴포넌트 설계 + +#### 3.4.1 메인 컴포넌트 (`index.tsx`) + +`UniversalListPage` 패턴을 따르되, **통계 카드 + 필터 + 테이블**을 포함한다. + +```typescript +// 통계 카드 5개 +computeStats: (data, totalCount, stats) => [ + { label: '총 호출 수', value: stats.total_count, icon: Activity }, + { label: '입력 토큰', value: formatNumber(stats.total_prompt_tokens), icon: ArrowUpRight }, + { label: '출력 토큰', value: formatNumber(stats.total_completion_tokens), icon: ArrowDownRight }, + { label: '총 비용 (USD)', value: formatUsd(stats.total_cost_usd), icon: DollarSign }, + { label: '총 비용 (KRW)', value: formatKrw(stats.total_cost_krw), icon: Coins }, +] +``` + +#### 3.4.2 테이블 컬럼 + +```typescript +columns: [ + { key: 'no', label: 'No.', className: 'text-center w-[50px]' }, + { key: 'created_at', label: '사용일시', className: 'min-w-[140px]' }, + { key: 'category', label: '카테고리', className: 'min-w-[100px]' }, + { key: 'menu_name', label: '호출메뉴', className: 'min-w-[120px]' }, + { key: 'model', label: '모델', className: 'min-w-[140px]' }, + { key: 'prompt_tokens', label: '입력토큰', className: 'text-right min-w-[90px]' }, + { key: 'completion_tokens', label: '출력토큰', className: 'text-right min-w-[90px]' }, + { key: 'total_tokens', label: '전체토큰', className: 'text-right min-w-[90px]' }, + { key: 'cost_usd', label: '비용(USD)', className: 'text-right min-w-[90px]' }, + { key: 'cost_krw', label: '비용(KRW)', className: 'text-right min-w-[90px]' }, + { key: 'creator', label: '사용자', className: 'min-w-[80px]' }, +] +``` + +#### 3.4.3 필터 (서버 사이드) + +```typescript +// 서버 사이드 필터링 — API에 쿼리 파라미터로 전달 +interface AiTokenUsageFilters { + start_date?: string; // YYYY-MM-DD + end_date?: string; // YYYY-MM-DD + menu_name?: string; // 드롭다운 선택 + per_page?: number; // 기본 20 + page?: number; +} +``` + +> MNG와 달리 **테넌트 필터 없음** — API가 `tenantId()`로 자동 필터링한다. + +#### 3.4.4 카테고리 매핑 (`AiTokenUsageConfig.ts`) + +```typescript +// MNG와 동일한 카테고리 분류 로직 +export 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; +} + +// 포맷팅 유틸 +export const formatNumber = (n: number) => n.toLocaleString('ko-KR'); +export const formatUsd = (n: number) => `$${n.toFixed(4)}`; +export const formatKrw = (n: number) => `${Math.round(n).toLocaleString('ko-KR')}원`; +``` + +#### 3.4.5 Server Actions (`actions.ts`) + +```typescript +'use server'; + +import { executeServerAction } from '@/lib/api/execute-server-action'; +import { buildApiUrl } from '@/lib/api/build-api-url'; + +export async function getAiTokenUsageList(params: AiTokenUsageFilters) { + return executeServerAction({ + url: buildApiUrl('/api/v1/settings/ai-token-usage', params), + errorMessage: 'AI 토큰 사용량 조회에 실패했습니다.', + }); +} + +export async function getAiTokenUsagePricing() { + return executeServerAction({ + url: buildApiUrl('/api/v1/settings/ai-token-usage/pricing'), + errorMessage: 'AI 단가 조회에 실패했습니다.', + }); +} +``` + +#### 3.4.6 단가 조회 모달 (`PricingModal.tsx`) + +MNG의 단가 설정 모달과 동일한 UI이지만 **읽기 전용**: + +``` +┌─────────────────────────────────────────────┐ +│ AI 단가 설정 (읽기 전용) [닫기] │ +├─────────────────────────────────────────────┤ +│ │ +│ ■ AI 토큰 단가 │ +│ ┌───────┬──────┬──────────┬──────────┐ │ +│ │Provider│ 모델 │입력($/1M)│출력($/1M)│ │ +│ ├───────┼──────┼──────────┼──────────┤ │ +│ │Gemini │flash │ $0.10 │ $0.40 │ │ +│ │Claude │haiku │ $0.25 │ $1.25 │ │ +│ └───────┴──────┴──────────┴──────────┘ │ +│ │ +│ ■ 저장소/서비스 단가 │ +│ ┌───────┬─────────┬────────┐ │ +│ │Provider│ 단위가격 │ 단위 │ │ +│ ├───────┼─────────┼────────┤ │ +│ │R2 │ $0.0045 │1M 작업 │ │ +│ └───────┴─────────┴────────┘ │ +│ │ +│ 환율: 1 USD = 1,400 KRW │ +│ │ +│ ※ 단가 변경은 관리자에게 문의하세요 │ +└─────────────────────────────────────────────┘ +``` + +### 3.5 타입 정의 (`types.ts`) + +```typescript +export 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; + creator?: { id: number; name: string }; + created_at: string; +} + +export 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; +} + +export interface AiPricingItem { + 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; +} +``` + +### 3.6 파일 목록 (sam/react) + +| 상태 | 파일 | 설명 | +|------|------|------| +| 신규 | `src/app/[locale]/(protected)/settings/ai-token-usage/page.tsx` | 페이지 | +| 신규 | `src/components/settings/AiTokenUsage/index.tsx` | 메인 컴포넌트 | +| 신규 | `src/components/settings/AiTokenUsage/actions.ts` | Server Actions | +| 신규 | `src/components/settings/AiTokenUsage/types.ts` | 타입 정의 | +| 신규 | `src/components/settings/AiTokenUsage/AiTokenUsageConfig.ts` | 상수/유틸 | +| 신규 | `src/components/settings/AiTokenUsage/PricingModal.tsx` | 단가 조회 모달 | + +--- + +## 4. DB 변경 + +### 4.1 테이블 변경 없음 + +`ai_token_usages`, `ai_pricing_configs` 테이블은 이미 존재하며 변경 불필요. + +### 4.2 데이터 추가 + +`ai_pricing_configs` 테이블에 `cloudflare-r2` provider 레코드 1건 추가: + +```bash +docker exec sam-api-1 php artisan tinker --execute=" +\App\Models\Tenants\AiPricingConfig::create([ + 'provider' => 'cloudflare-r2', + 'model_name' => 'cloud-storage', + 'input_price_per_million' => 0, + 'output_price_per_million' => 0, + 'unit_price' => 0.0045, + 'unit_description' => 'per 1,000,000 Class A operations', + 'exchange_rate' => 1400, + 'is_active' => true, + 'description' => 'Cloudflare R2 Storage (S3 compatible)', +]); +" +``` + +### 4.3 AiPricingConfig 캐시 키 확장 + +`AiPricingConfig::clearCache()`에 `cloudflare-r2` provider 추가: + +```php +// 기존: ['gemini', 'claude', 'google-stt', 'google-gcs'] +// 변경: ['gemini', 'claude', 'google-stt', 'google-gcs', 'cloudflare-r2'] +``` + +--- + +## 5. 메뉴 등록 + +### 5.1 React 메뉴 (서비스) + +서비스 메뉴 DB에 등록 (tinker 사용): + +```bash +# 설정 부모 메뉴 ID 조회 후 하위에 추가 +# (React 메뉴 체계는 별도 확인 필요) +``` + +--- + +## 6. MNG vs 서비스 기능 비교 + +| 기능 | MNG | 서비스 | 비고 | +|------|:---:|:------:|------| +| 사용량 목록 조회 | ✅ | ✅ | | +| 통계 카드 (6종) | ✅ | ✅ (5종) | 서비스: 전체토큰 카드 제외 (간결화) | +| 기간 필터 | ✅ | ✅ | | +| 테넌트 필터 | ✅ | ❌ | 서비스: 자기 테넌트 자동 필터 | +| 메뉴명 필터 | ✅ | ✅ | | +| 카테고리 분류 | ✅ | ✅ | 동일 로직 | +| 녹음시간 표시 | ✅ | ❌ | 서비스: STT 미사용 | +| 단가 설정 편집 | ✅ | ❌ | 서비스: 조회만 (MNG에서 관리) | +| 단가 조회 | ✅ | ✅ | | +| GCS 비용 추적 | ✅ | ❌ | 서비스: R2 비용 추적 | +| R2 비용 추적 | ❌ | ✅ | 신규 | +| 페이지네이션 | ✅ | ✅ | | +| 사용자명 표시 | ❌ | ✅ | creator 관계 로드 | + +--- + +## 7. 구현 순서 + +### Phase 1: API (sam/api) + +1. `AiTokenHelper` 생성 (MNG 코드 기반 + `saveR2StorageUsage` 추가) +2. `AiTokenUsageService` 생성 +3. `AiTokenUsageListRequest` 생성 +4. `AiTokenUsageController` 생성 +5. `common.php` 라우트 등록 +6. `ai_pricing_configs`에 `cloudflare-r2` 데이터 추가 (tinker) +7. Swagger 문서 작성 + +### Phase 2: React (sam/react) + +1. 타입 정의 (`types.ts`) +2. 설정/유틸 (`AiTokenUsageConfig.ts`) +3. Server Actions (`actions.ts`) +4. 단가 조회 모달 (`PricingModal.tsx`) +5. 메인 컴포넌트 (`index.tsx`) +6. 페이지 파일 (`page.tsx`) +7. 메뉴 등록 (DB tinker) + +--- + +## 관련 문서 + +- [features/ai/README.md](../features/ai/README.md) — AI 분석 리포트 기능 문서 +- [dev/guides/file-storage-guide.md](../dev/guides/file-storage-guide.md) — 파일 저장소 정책 +- [guides/ai-management.md](../guides/ai-management.md) — AI 관리 가이드 + +--- + +**최종 업데이트**: 2026-03-18