Initial commit: 프로젝트 초기 구성

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-02-09 08:21:50 +09:00
commit 0ee5d066b4
20 changed files with 2981 additions and 0 deletions

View File

@ -0,0 +1,269 @@
# 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}
```