docs: [plans] 이용현황 React 구현 요청서 추가
- API 응답 구조 (ai_tokens 신규, api_calls 제거) - TypeScript 타입 정의 (UsageApiData, AiTokenByModel) - 화면 와이어프레임 및 Progress Bar 색상 규칙 - 컴포넌트 구조 및 파일 변경 목록 - 체크리스트 11항목
This commit is contained in:
1
INDEX.md
1
INDEX.md
@@ -297,6 +297,7 @@ DB 도메인별:
|
||||
| [ai-token-usage-service-migration.md](plans/ai-token-usage-service-migration.md) | AI 토큰사용량 서비스 이관 기획 (MNG→API+React, GCS→R2 저장소 차이) |
|
||||
| [notification-sound-react-request.md](plans/notification-sound-react-request.md) | 알림설정 soundType React 구현 요청 (음원 배치, 미리듣기 실제 재생, API 연동 완료) |
|
||||
| [usage-subscription-unification.md](plans/usage-subscription-unification.md) | 이용현황+구독관리 통합 계획 (AI 토큰 사용량, 저장공간, 메뉴 통합) |
|
||||
| [usage-react-request.md](plans/usage-react-request.md) | 이용현황 React 구현 요청서 (API 완료, 타입/컴포넌트/와이어프레임 포함) |
|
||||
|
||||
### frontend/integration/ — 프론트엔드 개발 가이드
|
||||
|
||||
|
||||
440
plans/usage-react-request.md
Normal file
440
plans/usage-react-request.md
Normal file
@@ -0,0 +1,440 @@
|
||||
# 이용현황 페이지 React 구현 요청서
|
||||
|
||||
> **작성일**: 2026-03-18
|
||||
> **상태**: API 구현 완료, React 구현 대기
|
||||
> **대상**: sam/react
|
||||
> **메뉴 위치**: 사이드바 `이용현황` (`/usage`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목적
|
||||
|
||||
기존 **구독관리**(`/subscription`)와 **이용현황**(`/usage`)을 하나의 페이지로 통합한다. 구독 정보, 리소스 사용량(사용자/저장공간), AI 토큰 사용량을 한 화면에 표시한다.
|
||||
|
||||
### 1.2 변경 요약
|
||||
|
||||
| 항목 | 변경 전 | 변경 후 |
|
||||
|------|--------|--------|
|
||||
| `/usage` | EmptyPage (미구현) | **통합 이용현황 페이지** |
|
||||
| `/subscription` | SubscriptionManagement | `/usage`로 리다이렉트 |
|
||||
| 메뉴 | 구독관리 + 이용현황 2개 | **이용현황** 1개로 통합 |
|
||||
| API 호출 수 (`api_calls`) | 하드코딩 10,000 | **제거** → AI 토큰으로 대체 |
|
||||
| AI 토큰 | 없음 | **신규 추가** (한도/비용/모델별) |
|
||||
| 저장공간 한도 | 10GB | **100GB** |
|
||||
|
||||
### 1.3 API 상태
|
||||
|
||||
| API | 엔드포인트 | 상태 |
|
||||
|-----|----------|:----:|
|
||||
| 구독 정보 | `GET /api/v1/subscriptions/current` | 기존 유지 |
|
||||
| **사용량 조회** | `GET /api/v1/subscriptions/usage` | **개선 완료** |
|
||||
| 구독 취소 | `POST /api/v1/subscriptions/{id}/cancel` | 기존 유지 |
|
||||
| 데이터 내보내기 | `POST /api/v1/subscriptions/export` | 기존 유지 |
|
||||
|
||||
---
|
||||
|
||||
## 2. API 응답 구조 (변경된 부분)
|
||||
|
||||
### 2.1 `GET /api/v1/subscriptions/usage`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"users": {
|
||||
"used": 24,
|
||||
"limit": 10,
|
||||
"percentage": 240.0
|
||||
},
|
||||
"storage": {
|
||||
"used": 22808833,
|
||||
"used_formatted": "21.75 MB",
|
||||
"limit": 107374182400,
|
||||
"limit_formatted": "100 GB",
|
||||
"percentage": 0.0
|
||||
},
|
||||
"ai_tokens": {
|
||||
"period": "2026-03",
|
||||
"total_requests": 156,
|
||||
"total_tokens": 620000,
|
||||
"prompt_tokens": 412000,
|
||||
"completion_tokens": 208000,
|
||||
"limit": 1000000,
|
||||
"percentage": 62.0,
|
||||
"cost_usd": 0.95,
|
||||
"cost_krw": 1234,
|
||||
"warning_threshold": 80,
|
||||
"is_over_limit": false,
|
||||
"by_model": [
|
||||
{
|
||||
"model": "gemini-2.0-flash",
|
||||
"requests": 120,
|
||||
"total_tokens": 496000,
|
||||
"cost_krw": 987
|
||||
},
|
||||
{
|
||||
"model": "claude-3-haiku",
|
||||
"requests": 36,
|
||||
"total_tokens": 124000,
|
||||
"cost_krw": 247
|
||||
}
|
||||
]
|
||||
},
|
||||
"subscription": {
|
||||
"plan": "스탠다드",
|
||||
"monthly_fee": 79000,
|
||||
"status": "active",
|
||||
"started_at": "2026-01-01",
|
||||
"ended_at": "2026-12-31",
|
||||
"remaining_days": 288
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 변경점 (기존 대비)
|
||||
|
||||
| 필드 | 변경 |
|
||||
|------|------|
|
||||
| `api_calls` | **삭제됨** |
|
||||
| `ai_tokens` | **신규 추가** |
|
||||
| `ai_tokens.limit` | 월별 토큰 한도 (테넌트별, 기본 100만) |
|
||||
| `ai_tokens.percentage` | 한도 대비 사용율 |
|
||||
| `ai_tokens.warning_threshold` | 경고 기준 (80%) |
|
||||
| `ai_tokens.is_over_limit` | 한도 초과 여부 |
|
||||
| `ai_tokens.by_model` | 모델별 사용량 내역 |
|
||||
| `subscription.plan` | Plan 테이블의 `name` (한글) |
|
||||
| `subscription.monthly_fee` | Plan 테이블의 `price` (원화) |
|
||||
| `subscription.started_at` | `start_date` → `started_at` 변경 |
|
||||
| `subscription.ended_at` | `next_billing` → `ended_at` 변경 |
|
||||
| `storage.limit` | 기본값 10GB → **100GB** |
|
||||
|
||||
---
|
||||
|
||||
## 3. TypeScript 타입 정의
|
||||
|
||||
### 3.1 `UsageApiData` 타입 (변경)
|
||||
|
||||
```typescript
|
||||
// 기존 api_calls 제거, ai_tokens 추가
|
||||
export interface UsageApiData {
|
||||
users: {
|
||||
used: number;
|
||||
limit: number;
|
||||
percentage: number;
|
||||
};
|
||||
storage: {
|
||||
used: number;
|
||||
used_formatted: string;
|
||||
limit: number;
|
||||
limit_formatted: string;
|
||||
percentage: number;
|
||||
};
|
||||
ai_tokens: {
|
||||
period: string; // "2026-03"
|
||||
total_requests: number;
|
||||
total_tokens: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
limit: number; // 월별 한도 (기본 100만)
|
||||
percentage: number; // 한도 대비 사용율
|
||||
cost_usd: number;
|
||||
cost_krw: number;
|
||||
warning_threshold: number; // 경고 기준 (80)
|
||||
is_over_limit: boolean;
|
||||
by_model: AiTokenByModel[];
|
||||
};
|
||||
subscription: {
|
||||
plan: string | null; // "스탠다드", "프로페셔널" 등
|
||||
monthly_fee: number; // 원화 금액
|
||||
status: string;
|
||||
started_at: string | null;
|
||||
ended_at: string | null;
|
||||
remaining_days: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AiTokenByModel {
|
||||
model: string; // "gemini-2.0-flash"
|
||||
requests: number;
|
||||
total_tokens: number;
|
||||
cost_krw: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 `SubscriptionInfo` 타입 (변경)
|
||||
|
||||
```typescript
|
||||
export interface SubscriptionInfo {
|
||||
// 구독
|
||||
id?: number;
|
||||
plan: string | null;
|
||||
planName: string;
|
||||
monthlyFee: number;
|
||||
status: SubscriptionStatus;
|
||||
startedAt: string | null;
|
||||
endedAt: string | null;
|
||||
remainingDays: number | null;
|
||||
|
||||
// 사용자
|
||||
userCount: number;
|
||||
userLimit: number;
|
||||
|
||||
// 저장공간
|
||||
storageUsed: number;
|
||||
storageLimit: number;
|
||||
storageUsedFormatted: string;
|
||||
storageLimitFormatted: string;
|
||||
storagePercentage: number;
|
||||
|
||||
// AI 토큰 (신규)
|
||||
aiTokens: {
|
||||
period: string;
|
||||
totalTokens: number;
|
||||
limit: number;
|
||||
percentage: number;
|
||||
costKrw: number;
|
||||
warningThreshold: number;
|
||||
isOverLimit: boolean;
|
||||
byModel: AiTokenByModel[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 화면 구성
|
||||
|
||||
### 4.1 전체 레이아웃
|
||||
|
||||
```
|
||||
이용현황
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ [섹션 1] 구독 정보 │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 요금제 │ │ 구독 상태 │ │ 구독 금액 │ │
|
||||
│ │ 스탠다드 │ │ ● 활성 │ │ ₩79,000/월 │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ 시작: 01-01 │ │ 남은일: 288일│ │ 종료: 12-31 │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ [섹션 2] 리소스 사용량 │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 사용자 │ │
|
||||
│ │ ████████████████████████░░░░░░ 24 / 10명 │ │
|
||||
│ │ │ │
|
||||
│ │ 저장 공간 │ │
|
||||
│ │ █░░░░░░░░░░░░░░░░░░░░░░░░░░░ 21.75 MB / 100 GB │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [섹션 3] AI 토큰 사용량 — 2026년 3월 │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ ██████████████████░░░░░░░░░░░ 620,000 / 1,000,000│ │
|
||||
│ │ 62.0% │ │
|
||||
│ │ │ │
|
||||
│ │ 총 비용: ₩1,234 │ │
|
||||
│ │ │ │
|
||||
│ │ 모델별 사용량: │ │
|
||||
│ │ ┌────────────────────┬────────┬──────┬───────┐ │ │
|
||||
│ │ │ 모델 │ 호출수 │ 토큰 │ 비용 │ │ │
|
||||
│ │ ├────────────────────┼────────┼──────┼───────┤ │ │
|
||||
│ │ │ gemini-2.0-flash │ 120 │ 496K │ ₩987 │ │ │
|
||||
│ │ │ claude-3-haiku │ 36 │ 124K │ ₩247 │ │ │
|
||||
│ │ └────────────────────┴────────┴──────┴───────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ※ 기본 제공: 월 100만 토큰. 초과 시 실비 과금 │ │
|
||||
│ │ ※ 매월 1일 리셋, 잔여 토큰 이월 불가 │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [섹션 4] 서비스 관리 │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ [ 자료 내보내기 ] [ 서비스 해지 ] │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Progress Bar 색상 규칙
|
||||
|
||||
| 사용율 | 색상 | 비고 |
|
||||
|--------|------|------|
|
||||
| 0~60% | `bg-blue-500` (기본) | 정상 |
|
||||
| 60~80% | `bg-yellow-500` (주의) | |
|
||||
| 80~100% | `bg-orange-500` (경고) | `warning_threshold` 기준 |
|
||||
| 100%+ | `bg-red-500` (초과) | `is_over_limit: true` |
|
||||
|
||||
### 4.3 AI 토큰 경고 표시
|
||||
|
||||
```typescript
|
||||
// percentage >= warning_threshold (80%)이면 경고 배지 표시
|
||||
{aiTokens.percentage >= aiTokens.warningThreshold && (
|
||||
<Badge variant="warning">
|
||||
기본 제공량의 {aiTokens.percentage}% 사용 중
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
// is_over_limit이면 초과 배지 표시
|
||||
{aiTokens.isOverLimit && (
|
||||
<Badge variant="destructive">
|
||||
한도 초과 — 초과분 실비 과금
|
||||
</Badge>
|
||||
)}
|
||||
```
|
||||
|
||||
### 4.4 구독 정보가 없는 경우
|
||||
|
||||
`subscription.plan`이 null이면 구독 카드 영역에 안내 메시지 표시:
|
||||
|
||||
```
|
||||
구독 정보가 없습니다. 관리자에게 문의하세요.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 컴포넌트 구조
|
||||
|
||||
### 5.1 파일 구조
|
||||
|
||||
```
|
||||
src/app/[locale]/(protected)/
|
||||
├── usage/page.tsx ← 신규: 이용현황 페이지
|
||||
└── subscription/page.tsx ← 수정: /usage로 리다이렉트
|
||||
|
||||
src/components/settings/SubscriptionManagement/
|
||||
├── types.ts ← 수정: UsageApiData에 ai_tokens 추가
|
||||
├── actions.ts ← 수정: API_URL → buildApiUrl
|
||||
├── utils.ts ← 수정: transformApiToFrontend 개선
|
||||
├── SubscriptionManagement.tsx ← 수정 또는 교체: AI 토큰 섹션 추가
|
||||
├── SubscriptionClient.tsx ← 수정 또는 교체
|
||||
└── index.ts ← 기존 유지
|
||||
```
|
||||
|
||||
### 5.2 Server Actions 변경
|
||||
|
||||
```typescript
|
||||
// actions.ts — buildApiUrl 사용으로 변경
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
|
||||
export async function getUsage(): Promise<ActionResult<UsageApiData>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/subscriptions/usage'),
|
||||
errorMessage: '사용량 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
> `getSubscriptionData()` 함수에서 `getCurrentSubscription()` + `getUsage()` 병렬 호출 유지. 단, `transformApiToFrontend()`에서 `ai_tokens` 필드를 매핑해야 한다.
|
||||
|
||||
### 5.3 `/usage/page.tsx` (신규)
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSubscriptionData } from '@/components/settings/SubscriptionManagement/actions';
|
||||
import type { SubscriptionInfo } from '@/components/settings/SubscriptionManagement/types';
|
||||
// 기존 SubscriptionManagement 컴포넌트 재사용
|
||||
import { SubscriptionManagement } from '@/components/settings/SubscriptionManagement';
|
||||
|
||||
export default function UsagePage() {
|
||||
const [data, setData] = useState<SubscriptionInfo | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getSubscriptionData()
|
||||
.then(result => setData(result.data))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <UsageSkeleton />;
|
||||
return <SubscriptionManagement data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 `/subscription/page.tsx` (리다이렉트)
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function SubscriptionRedirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.replace('/usage');
|
||||
}, [router]);
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 포맷팅 유틸
|
||||
|
||||
```typescript
|
||||
// 토큰 수 포맷: 1,000,000 → "1,000K" 또는 "1M"
|
||||
export function formatTokenCount(tokens: number): string {
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||
if (tokens >= 1_000) return `${Math.round(tokens / 1_000).toLocaleString()}K`;
|
||||
return tokens.toLocaleString();
|
||||
}
|
||||
|
||||
// 원화 포맷: 1234 → "₩1,234"
|
||||
export function formatKrw(amount: number): string {
|
||||
return `₩${Math.round(amount).toLocaleString()}`;
|
||||
}
|
||||
|
||||
// 월 표시: "2026-03" → "2026년 3월"
|
||||
export function formatPeriod(period: string): string {
|
||||
const [year, month] = period.split('-');
|
||||
return `${year}년 ${parseInt(month)}월`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 제거 대상
|
||||
|
||||
기존 코드에서 제거하거나 대체할 항목:
|
||||
|
||||
| 파일 | 제거 대상 | 사유 |
|
||||
|------|----------|------|
|
||||
| `types.ts` | `api_calls` 인터페이스 | `ai_tokens`로 대체 |
|
||||
| `types.ts` | `apiCallsUsed`, `apiCallsLimit` | 제거 |
|
||||
| `SubscriptionManagement.tsx` | API 호출 수 Progress Bar | AI 토큰 Progress Bar로 대체 |
|
||||
| `utils.ts` | `apiCallsUsed/Limit` 변환 | `aiTokens` 변환으로 대체 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 체크리스트
|
||||
|
||||
- [ ] `types.ts` — `UsageApiData`에서 `api_calls` 제거, `ai_tokens` + `subscription` 변경
|
||||
- [ ] `utils.ts` — `transformApiToFrontend()`에 AI 토큰 매핑 추가
|
||||
- [ ] `SubscriptionManagement.tsx` — AI 토큰 섹션 추가, API 호출 수 섹션 제거
|
||||
- [ ] Progress Bar — 사용율별 색상 변경 (0~60 파랑, 60~80 노랑, 80~100 주황, 100+ 빨강)
|
||||
- [ ] `by_model` 테이블 — 모델별 호출수/토큰/비용 표시
|
||||
- [ ] 한도 초과 경고 배지 — `is_over_limit`/`warning_threshold` 기반
|
||||
- [ ] `/usage/page.tsx` 신규 생성
|
||||
- [ ] `/subscription/page.tsx` → `/usage` 리다이렉트
|
||||
- [ ] `actions.ts` — `buildApiUrl()` 사용으로 변경
|
||||
- [ ] 구독 정보 null 처리 (미가입 테넌트)
|
||||
- [ ] 로딩 스켈레톤 표시
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
| 문서 | 경로 |
|
||||
|------|------|
|
||||
| 통합 계획 (내부) | `plans/usage-subscription-unification.md` |
|
||||
| 과금 정책 | `rules/customer-pricing.md` (6장 사용량 기반 추가 과금) |
|
||||
| AI 토큰 기능 | `features/ai/README.md` |
|
||||
| AI 토큰 API 명세 | `frontend/api-specs/ai-token-usage-api.md` |
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-18
|
||||
Reference in New Issue
Block a user