- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
248 lines
7.4 KiB
Python
248 lines
7.4 KiB
Python
"""
|
|
Exchange Rate API - 환율 정보 조회 (한국수출입은행 API 연동)
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
from datetime import datetime
|
|
|
|
from ..database import get_db
|
|
from ..models.exchange_rate import ExchangeRate, ExchangeRateHistory
|
|
from ..models.user import User
|
|
from .auth import get_current_admin_user
|
|
from ..services.exchange_rate_service import (
|
|
update_exchange_rates,
|
|
get_all_exchange_rates,
|
|
convert_krw_to_currency,
|
|
SUPPORTED_CURRENCIES
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/exchange-rate", tags=["Exchange Rate"])
|
|
|
|
|
|
class ExchangeRateData(BaseModel):
|
|
currency_code: str
|
|
currency_name: str
|
|
symbol: str
|
|
deal_base_rate: float # 매매기준율 (1 USD = X KRW)
|
|
ttb_rate: float # 전신환 받을때
|
|
tts_rate: float # 전신환 보낼때
|
|
weight_percent: float # 가중치 (%)
|
|
adjusted_rate: float # 가중치 적용 환율
|
|
source_date: str
|
|
updated_at: str
|
|
|
|
|
|
class ExchangeRatesResponse(BaseModel):
|
|
base_currency: str
|
|
rates: List[ExchangeRateData]
|
|
source: str
|
|
last_updated: str
|
|
|
|
|
|
class ExchangeRateWeightUpdate(BaseModel):
|
|
currency_code: str
|
|
weight_percent: float
|
|
|
|
|
|
class ConvertRequest(BaseModel):
|
|
amount: float
|
|
from_currency: str = "KRW"
|
|
to_currency: str
|
|
|
|
|
|
class ConvertResponse(BaseModel):
|
|
original_amount: float
|
|
from_currency: str
|
|
converted_amount: float
|
|
to_currency: str
|
|
rate_used: float
|
|
|
|
|
|
@router.get("", response_model=ExchangeRatesResponse)
|
|
async def get_exchange_rates(db: Session = Depends(get_db)):
|
|
"""환율 정보 조회"""
|
|
|
|
rates = get_all_exchange_rates(db)
|
|
|
|
# DB에 데이터가 없으면 업데이트 시도
|
|
if not rates:
|
|
await update_exchange_rates(db)
|
|
rates = get_all_exchange_rates(db)
|
|
|
|
rate_list = []
|
|
for rate in rates:
|
|
symbol = SUPPORTED_CURRENCIES.get(rate.currency_code, {}).get("symbol", "")
|
|
rate_list.append(ExchangeRateData(
|
|
currency_code=rate.currency_code,
|
|
currency_name=rate.currency_name,
|
|
symbol=symbol,
|
|
deal_base_rate=rate.deal_base_rate,
|
|
ttb_rate=rate.ttb_rate or rate.deal_base_rate,
|
|
tts_rate=rate.tts_rate or rate.deal_base_rate,
|
|
weight_percent=rate.weight_percent or 0.0,
|
|
adjusted_rate=rate.adjusted_rate or rate.deal_base_rate,
|
|
source_date=rate.source_date or "",
|
|
updated_at=rate.updated_at.isoformat() if rate.updated_at else ""
|
|
))
|
|
|
|
last_updated = ""
|
|
if rates:
|
|
latest = max(rates, key=lambda r: r.updated_at if r.updated_at else datetime.min)
|
|
last_updated = latest.updated_at.isoformat() if latest.updated_at else ""
|
|
|
|
return ExchangeRatesResponse(
|
|
base_currency="KRW",
|
|
rates=rate_list,
|
|
source="koreaexim",
|
|
last_updated=last_updated
|
|
)
|
|
|
|
|
|
@router.get("/currency/{currency_code}")
|
|
async def get_single_rate(currency_code: str, db: Session = Depends(get_db)):
|
|
"""특정 통화 환율 조회"""
|
|
rate = db.query(ExchangeRate).filter(
|
|
ExchangeRate.currency_code == currency_code.upper(),
|
|
ExchangeRate.is_active == True
|
|
).first()
|
|
|
|
if not rate:
|
|
raise HTTPException(status_code=404, detail=f"Currency {currency_code} not found")
|
|
|
|
symbol = SUPPORTED_CURRENCIES.get(rate.currency_code, {}).get("symbol", "")
|
|
|
|
return {
|
|
"currency_code": rate.currency_code,
|
|
"currency_name": rate.currency_name,
|
|
"symbol": symbol,
|
|
"deal_base_rate": rate.deal_base_rate,
|
|
"adjusted_rate": rate.adjusted_rate,
|
|
"weight_percent": rate.weight_percent,
|
|
"source_date": rate.source_date,
|
|
"updated_at": rate.updated_at.isoformat() if rate.updated_at else None
|
|
}
|
|
|
|
|
|
@router.post("/convert", response_model=ConvertResponse)
|
|
async def convert_currency(
|
|
request: ConvertRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""통화 변환"""
|
|
if request.from_currency.upper() != "KRW":
|
|
raise HTTPException(status_code=400, detail="Currently only KRW conversion is supported")
|
|
|
|
converted = convert_krw_to_currency(db, request.amount, request.to_currency.upper())
|
|
|
|
if converted is None:
|
|
raise HTTPException(status_code=404, detail=f"Currency {request.to_currency} not found")
|
|
|
|
rate = db.query(ExchangeRate).filter(
|
|
ExchangeRate.currency_code == request.to_currency.upper()
|
|
).first()
|
|
|
|
return ConvertResponse(
|
|
original_amount=request.amount,
|
|
from_currency=request.from_currency.upper(),
|
|
converted_amount=round(converted, 2),
|
|
to_currency=request.to_currency.upper(),
|
|
rate_used=rate.adjusted_rate if rate else 0
|
|
)
|
|
|
|
|
|
@router.get("/weights")
|
|
async def get_exchange_rate_weights(db: Session = Depends(get_db)):
|
|
"""환율 가중치 설정 조회"""
|
|
rates = get_all_exchange_rates(db)
|
|
|
|
return {
|
|
rate.currency_code.lower(): rate.weight_percent or 0.0
|
|
for rate in rates
|
|
}
|
|
|
|
|
|
@router.put("/weights/{currency_code}")
|
|
async def update_exchange_rate_weight(
|
|
currency_code: str,
|
|
weight_percent: float,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_user)
|
|
):
|
|
"""환율 가중치 수정 (관리자 전용)"""
|
|
rate = db.query(ExchangeRate).filter(
|
|
ExchangeRate.currency_code == currency_code.upper()
|
|
).first()
|
|
|
|
if not rate:
|
|
raise HTTPException(status_code=404, detail=f"Currency {currency_code} not found")
|
|
|
|
rate.weight_percent = weight_percent
|
|
rate.adjusted_rate = rate.deal_base_rate * (1 + weight_percent / 100)
|
|
|
|
db.commit()
|
|
|
|
return {
|
|
"message": "Weight updated successfully",
|
|
"currency_code": rate.currency_code,
|
|
"weight_percent": rate.weight_percent,
|
|
"adjusted_rate": rate.adjusted_rate
|
|
}
|
|
|
|
|
|
@router.post("/refresh")
|
|
async def refresh_exchange_rates(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_user)
|
|
):
|
|
"""환율 강제 갱신 (관리자 전용)"""
|
|
result = await update_exchange_rates(db, force=True)
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/history/{currency_code}")
|
|
async def get_exchange_rate_history(
|
|
currency_code: str,
|
|
limit: int = 30,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""환율 변동 이력 조회"""
|
|
history = db.query(ExchangeRateHistory).filter(
|
|
ExchangeRateHistory.currency_code == currency_code.upper()
|
|
).order_by(ExchangeRateHistory.created_at.desc()).limit(limit).all()
|
|
|
|
return [
|
|
{
|
|
"currency_code": h.currency_code,
|
|
"deal_base_rate": h.deal_base_rate,
|
|
"source_date": h.source_date,
|
|
"created_at": h.created_at.isoformat() if h.created_at else None
|
|
}
|
|
for h in history
|
|
]
|
|
|
|
|
|
# 프론트엔드용 간단 API
|
|
@router.get("/simple")
|
|
async def get_simple_rates(db: Session = Depends(get_db)):
|
|
"""프론트엔드용 간단 환율 정보"""
|
|
rates = get_all_exchange_rates(db)
|
|
|
|
# DB에 데이터가 없으면 업데이트 시도
|
|
if not rates:
|
|
await update_exchange_rates(db)
|
|
rates = get_all_exchange_rates(db)
|
|
|
|
result = {}
|
|
for rate in rates:
|
|
result[rate.currency_code] = {
|
|
"rate": rate.adjusted_rate, # KRW per 1 unit (e.g., 1 USD = 1450 KRW)
|
|
"symbol": SUPPORTED_CURRENCIES.get(rate.currency_code, {}).get("symbol", ""),
|
|
"name": rate.currency_name
|
|
}
|
|
|
|
return result
|