Files
AutonetSellCar/backend/app/services/exchange_rate_service.py
AutonetSellCar Deploy 1f0dcb1ddb Initial commit: AutonetSellCar platform with deployment system
- 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>
2025-12-30 13:24:39 +09:00

306 lines
9.5 KiB
Python

"""
Exchange Rate Service - 한국수출입은행 API 연동
API 문서: https://www.koreaexim.go.kr/ir/HPHKIR020M01?apino=2&viewtype=C
"""
import httpx
import os
from datetime import datetime, timedelta
from typing import Optional, Dict, List
from sqlalchemy.orm import Session
from ..models.exchange_rate import ExchangeRate, ExchangeRateHistory
# 한국수출입은행 API 설정
KOREAEXIM_API_URL = "https://oapi.koreaexim.go.kr/site/program/financial/exchangeJSON"
KOREAEXIM_API_KEY = os.getenv("KOREAEXIM_API_KEY", "rOzKaATDEinF9luHla1wVTosjWribjKL")
# 지원 통화 목록
SUPPORTED_CURRENCIES = {
"USD": {"name_ko": "미국 달러", "name_en": "US Dollar", "symbol": "$"},
"MNT": {"name_ko": "몽골 투그릭", "name_en": "Mongolian Tugrik", "symbol": ""},
"RUB": {"name_ko": "러시아 루블", "name_en": "Russian Ruble", "symbol": ""},
"CNY": {"name_ko": "중국 위안", "name_en": "Chinese Yuan", "symbol": "¥"},
"JPY": {"name_ko": "일본 엔", "name_en": "Japanese Yen", "symbol": "¥"},
"EUR": {"name_ko": "유로", "name_en": "Euro", "symbol": ""},
}
# 기본 환율 (API 실패 시 사용, 2024년 12월 기준)
DEFAULT_RATES = {
"USD": 1450.0,
"MNT": 0.42, # 1 MNT = 0.42 KRW
"RUB": 14.0,
"CNY": 198.0,
"JPY": 9.5, # 100엔 기준이면 950
"EUR": 1510.0,
}
async def fetch_rates_from_koreaexim(search_date: Optional[str] = None) -> Optional[List[Dict]]:
"""
한국수출입은행 API에서 환율 정보 조회
Args:
search_date: 조회일자 (YYYYMMDD 형식), 없으면 오늘
Returns:
환율 데이터 리스트 또는 None
"""
if not KOREAEXIM_API_KEY:
print("Warning: KOREAEXIM_API_KEY not set, using fallback rates")
return None
if not search_date:
search_date = datetime.now().strftime("%Y%m%d")
try:
async with httpx.AsyncClient() as client:
response = await client.get(
KOREAEXIM_API_URL,
params={
"authkey": KOREAEXIM_API_KEY,
"searchdate": search_date,
"data": "AP01" # 환율 데이터
},
timeout=15.0
)
if response.status_code == 200:
data = response.json()
# API 결과 코드 확인
if isinstance(data, list) and len(data) > 0:
return data
else:
print(f"Korea Exim API returned empty data for date {search_date}")
# 주말/공휴일이면 이전 영업일 데이터 조회
return None
except Exception as e:
print(f"Failed to fetch from Korea Exim API: {e}")
return None
def parse_koreaexim_response(data: List[Dict]) -> Dict[str, Dict]:
"""
한국수출입은행 API 응답 파싱
Response format:
{
"result": 1,
"cur_unit": "USD",
"cur_nm": "미국 달러",
"ttb": "1,438.71", # 전신환(송금) 받을때
"tts": "1,467.28", # 전신환(송금) 보낼때
"deal_bas_r": "1,452.99", # 매매 기준율
"bkpr": "1,452", # 장부가격
...
}
"""
parsed = {}
for item in data:
try:
cur_unit = item.get("cur_unit", "").replace("(100)", "").strip()
if cur_unit not in SUPPORTED_CURRENCIES:
continue
# 쉼표 제거 후 숫자 변환
deal_base_rate = float(item.get("deal_bas_r", "0").replace(",", ""))
ttb_rate = float(item.get("ttb", "0").replace(",", ""))
tts_rate = float(item.get("tts", "0").replace(",", ""))
# 100엔 단위인 경우 (JPY(100))
if "(100)" in item.get("cur_unit", ""):
deal_base_rate /= 100
ttb_rate /= 100
tts_rate /= 100
parsed[cur_unit] = {
"currency_code": cur_unit,
"currency_name": item.get("cur_nm", SUPPORTED_CURRENCIES[cur_unit]["name_ko"]),
"deal_base_rate": deal_base_rate,
"ttb_rate": ttb_rate,
"tts_rate": tts_rate,
}
except (ValueError, KeyError) as e:
print(f"Error parsing currency {item.get('cur_unit')}: {e}")
continue
return parsed
async def update_exchange_rates(db: Session, force: bool = False) -> Dict:
"""
환율 정보 업데이트
Args:
db: DB 세션
force: 강제 업데이트 여부
Returns:
업데이트 결과
"""
today = datetime.now().strftime("%Y%m%d")
# 오늘 이미 업데이트했는지 확인 (force가 아닌 경우)
if not force:
existing = db.query(ExchangeRate).filter(
ExchangeRate.source_date == today
).first()
if existing:
return {
"status": "skipped",
"message": f"Already updated for {today}",
"source_date": today
}
# API 호출 (오늘 데이터 시도)
api_data = await fetch_rates_from_koreaexim(today)
source_date = today
# 오늘 데이터 없으면 어제 시도 (주말/공휴일 대응)
if not api_data:
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y%m%d")
api_data = await fetch_rates_from_koreaexim(yesterday)
source_date = yesterday
# 그래도 없으면 기본값 사용
if not api_data:
print("Using fallback rates")
rates_data = {
code: {
"currency_code": code,
"currency_name": info["name_ko"],
"deal_base_rate": DEFAULT_RATES.get(code, 1.0),
"ttb_rate": DEFAULT_RATES.get(code, 1.0) * 0.98,
"tts_rate": DEFAULT_RATES.get(code, 1.0) * 1.02,
}
for code, info in SUPPORTED_CURRENCIES.items()
}
source = "fallback"
else:
rates_data = parse_koreaexim_response(api_data)
source = "koreaexim"
# DB에 저장/업데이트
updated_currencies = []
for code, rate_info in rates_data.items():
existing = db.query(ExchangeRate).filter(
ExchangeRate.currency_code == code
).first()
if existing:
# 기존 데이터 업데이트
old_rate = existing.deal_base_rate
existing.currency_name = rate_info["currency_name"]
existing.deal_base_rate = rate_info["deal_base_rate"]
existing.ttb_rate = rate_info["ttb_rate"]
existing.tts_rate = rate_info["tts_rate"]
existing.adjusted_rate = rate_info["deal_base_rate"] * (1 + existing.weight_percent / 100)
existing.source_date = source_date
# 변동이 있으면 히스토리 저장
if old_rate != rate_info["deal_base_rate"]:
history = ExchangeRateHistory(
currency_code=code,
deal_base_rate=rate_info["deal_base_rate"],
source_date=source_date
)
db.add(history)
else:
# 신규 데이터 추가
new_rate = ExchangeRate(
currency_code=code,
currency_name=rate_info["currency_name"],
deal_base_rate=rate_info["deal_base_rate"],
ttb_rate=rate_info["ttb_rate"],
tts_rate=rate_info["tts_rate"],
weight_percent=0.0,
adjusted_rate=rate_info["deal_base_rate"],
source_date=source_date,
is_active=True
)
db.add(new_rate)
# 히스토리 저장
history = ExchangeRateHistory(
currency_code=code,
deal_base_rate=rate_info["deal_base_rate"],
source_date=source_date
)
db.add(history)
updated_currencies.append(code)
db.commit()
return {
"status": "success",
"message": f"Updated {len(updated_currencies)} currencies",
"currencies": updated_currencies,
"source": source,
"source_date": source_date
}
def get_exchange_rate(db: Session, currency_code: str) -> Optional[ExchangeRate]:
"""특정 통화 환율 조회"""
return db.query(ExchangeRate).filter(
ExchangeRate.currency_code == currency_code,
ExchangeRate.is_active == True
).first()
def get_all_exchange_rates(db: Session) -> List[ExchangeRate]:
"""모든 환율 조회"""
return db.query(ExchangeRate).filter(
ExchangeRate.is_active == True
).all()
def convert_krw_to_currency(db: Session, krw_amount: float, currency_code: str) -> Optional[float]:
"""
KRW를 다른 통화로 변환
Args:
db: DB 세션
krw_amount: 원화 금액
currency_code: 대상 통화 코드 (USD, MNT, RUB, CNY)
Returns:
변환된 금액 또는 None
"""
rate = get_exchange_rate(db, currency_code)
if not rate or rate.adjusted_rate <= 0:
return None
# KRW / 환율 = 외화
return krw_amount / rate.adjusted_rate
def convert_currency_to_krw(db: Session, amount: float, currency_code: str) -> Optional[float]:
"""
다른 통화를 KRW로 변환
Args:
db: DB 세션
amount: 외화 금액
currency_code: 원화 통화 코드
Returns:
KRW 금액 또는 None
"""
rate = get_exchange_rate(db, currency_code)
if not rate:
return None
# 외화 * 환율 = KRW
return amount * rate.adjusted_rate