- FastAPI 백엔드 (audio-studio-api) - Next.js 프론트엔드 (audio-studio-ui) - Qwen3-TTS 엔진 (audio-studio-tts) - MusicGen 서비스 (audio-studio-musicgen) - Docker Compose 개발/운영 환경 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
270 lines
6.7 KiB
Markdown
270 lines
6.7 KiB
Markdown
# RESTful API 설계 규칙 (API Design Standards)
|
|
|
|
이 프로젝트의 RESTful API 설계 패턴입니다.
|
|
|
|
## FastAPI 기본 구조
|
|
|
|
### 앱 초기화
|
|
```python
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
app = FastAPI(
|
|
title="Biocode API",
|
|
description="바이오코드 성격 분석 및 유명인 등록 API",
|
|
version="1.0.0"
|
|
)
|
|
```
|
|
|
|
### Pydantic 모델 정의
|
|
```python
|
|
class AddFamousPersonRequest(BaseModel):
|
|
year: int
|
|
month: int
|
|
day: int
|
|
name: str
|
|
description: Optional[str] = ""
|
|
entity_type: Optional[str] = "person" # "person" or "organization"
|
|
|
|
class BiocodeResponse(BaseModel):
|
|
biocode: str
|
|
g_code: str
|
|
s_code: str
|
|
personality: dict
|
|
```
|
|
|
|
## 엔드포인트 패턴
|
|
|
|
### Health Check
|
|
```python
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""헬스체크 엔드포인트"""
|
|
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
|
```
|
|
|
|
### GET - 조회
|
|
```python
|
|
@app.get("/biocode/{year}/{month}/{day}")
|
|
async def get_biocode(year: int, month: int, day: int):
|
|
"""생년월일로 바이오코드 조회"""
|
|
try:
|
|
biocode = calculate_biocode(year, month, day)
|
|
g_code = f"g{biocode[:2]}"
|
|
s_code = biocode[2:]
|
|
|
|
if g_code not in biocode_data:
|
|
raise HTTPException(status_code=404, detail=f"G code {g_code} not found")
|
|
|
|
personality = biocode_data[g_code].get("codes", {}).get(biocode, {})
|
|
|
|
return {
|
|
"biocode": biocode,
|
|
"g_code": g_code,
|
|
"s_code": s_code,
|
|
"personality": personality
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
```
|
|
|
|
### POST - 생성
|
|
```python
|
|
@app.post("/add_famous_person")
|
|
async def add_famous_person(request: AddFamousPersonRequest):
|
|
"""유명인/조직 등록"""
|
|
try:
|
|
biocode = calculate_biocode(request.year, request.month, request.day)
|
|
g_code = f"g{biocode[:2]}"
|
|
|
|
if g_code not in biocode_data:
|
|
raise HTTPException(status_code=404, detail=f"G code {g_code} not found")
|
|
|
|
# 엔티티 타입에 따라 저장 위치 결정
|
|
is_organization = request.entity_type == "organization"
|
|
target_field = "famousOrganizations" if is_organization else "famousPeople"
|
|
|
|
# 데이터 추가
|
|
new_entry = {
|
|
"name": request.name,
|
|
"code": biocode,
|
|
"description": request.description or ""
|
|
}
|
|
|
|
if target_field not in biocode_data[g_code]["codes"][biocode]:
|
|
biocode_data[g_code]["codes"][biocode][target_field] = []
|
|
|
|
biocode_data[g_code]["codes"][biocode][target_field].append(new_entry)
|
|
|
|
# JSON 파일 저장
|
|
save_biocode_data(g_code)
|
|
|
|
return {
|
|
"success": True,
|
|
"biocode": biocode,
|
|
"entity_type": request.entity_type,
|
|
"name": request.name
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
```
|
|
|
|
## 라우터 분리 패턴
|
|
|
|
### 메인 앱 (main.py)
|
|
```python
|
|
from fastapi import FastAPI
|
|
from app.routers import users, articles
|
|
|
|
app = FastAPI(title="News Engine API")
|
|
|
|
app.include_router(users.router, prefix="/api/users", tags=["users"])
|
|
app.include_router(articles.router, prefix="/api/articles", tags=["articles"])
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "healthy"}
|
|
```
|
|
|
|
### 라우터 (routers/users.py)
|
|
```python
|
|
from fastapi import APIRouter, HTTPException, Depends
|
|
from app.models.user import UserCreate, UserResponse
|
|
from app.database import get_database
|
|
|
|
router = APIRouter()
|
|
|
|
@router.post("/", response_model=UserResponse)
|
|
async def create_user(user: UserCreate, db = Depends(get_database)):
|
|
"""새 사용자 생성"""
|
|
existing = await db.users.find_one({"email": user.email})
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Email already registered")
|
|
|
|
result = await db.users.insert_one(user.dict())
|
|
return UserResponse(id=str(result.inserted_id), **user.dict())
|
|
|
|
@router.get("/{user_id}", response_model=UserResponse)
|
|
async def get_user(user_id: str, db = Depends(get_database)):
|
|
"""사용자 조회"""
|
|
user = await db.users.find_one({"_id": ObjectId(user_id)})
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return UserResponse(**user)
|
|
```
|
|
|
|
## 에러 처리
|
|
|
|
### HTTPException 사용
|
|
```python
|
|
from fastapi import HTTPException
|
|
|
|
# 404 Not Found
|
|
raise HTTPException(status_code=404, detail="Resource not found")
|
|
|
|
# 400 Bad Request
|
|
raise HTTPException(status_code=400, detail="Invalid input data")
|
|
|
|
# 500 Internal Server Error
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
```
|
|
|
|
### 전역 예외 처리
|
|
```python
|
|
from fastapi import Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
@app.exception_handler(Exception)
|
|
async def global_exception_handler(request: Request, exc: Exception):
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"detail": "Internal server error", "error": str(exc)}
|
|
)
|
|
```
|
|
|
|
## 응답 형식
|
|
|
|
### 성공 응답
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": { ... },
|
|
"message": "Operation completed successfully"
|
|
}
|
|
```
|
|
|
|
### 에러 응답
|
|
```json
|
|
{
|
|
"detail": "Error message here"
|
|
}
|
|
```
|
|
|
|
### 목록 응답
|
|
```json
|
|
{
|
|
"items": [...],
|
|
"total": 100,
|
|
"page": 1,
|
|
"page_size": 20
|
|
}
|
|
```
|
|
|
|
## CORS 설정
|
|
|
|
```python
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:3000", "https://yourdomain.com"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
```
|
|
|
|
## API 버저닝
|
|
|
|
```python
|
|
# URL 기반 버저닝
|
|
app.include_router(v1_router, prefix="/api/v1")
|
|
app.include_router(v2_router, prefix="/api/v2")
|
|
|
|
# 또는 헤더 기반
|
|
@app.get("/api/resource")
|
|
async def get_resource(api_version: str = Header(default="v1")):
|
|
if api_version == "v2":
|
|
return v2_response()
|
|
return v1_response()
|
|
```
|
|
|
|
## 인증
|
|
|
|
### JWT 토큰 검증
|
|
```python
|
|
from fastapi import Depends, HTTPException
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
import jwt
|
|
|
|
security = HTTPBearer()
|
|
|
|
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
try:
|
|
payload = jwt.decode(
|
|
credentials.credentials,
|
|
os.getenv("JWT_SECRET_KEY"),
|
|
algorithms=["HS256"]
|
|
)
|
|
return payload
|
|
except jwt.ExpiredSignatureError:
|
|
raise HTTPException(status_code=401, detail="Token expired")
|
|
except jwt.InvalidTokenError:
|
|
raise HTTPException(status_code=401, detail="Invalid token")
|
|
|
|
@router.get("/protected")
|
|
async def protected_route(user = Depends(verify_token)):
|
|
return {"user": user}
|
|
```
|