Coverage for view / web / api_common.py: 100%

51 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-04 15:08 +0000

1""" 

2웹 API 공통 유틸리티: 컨텍스트 관리, 응답 직렬화, 인증, Pydantic 모델. 

3""" 

4from fastapi import HTTPException, Request 

5from pydantic import BaseModel 

6 

7_ctx = None # 전역 변수로 선언 

8 

9# API 호출 실패 시 대처를 위한 전역 가격 캐시 (메모리) 

10_PRICE_CACHE = {} # {code: (price, rate, timestamp)} 

11 

12# 서버 측 in-flight 요청 추적 (hang 진단용): request_id → {path, method, start, query} 

13_active_requests: dict = {} 

14 

15# 최근 완료 요청 이력 (hang 직전 분석용): deque-like list, 최대 20건 

16_recent_completed: list = [] 

17_RECENT_MAX = 20 

18 

19 

20def set_ctx(ctx): 

21 global _ctx 

22 _ctx = ctx 

23 

24 

25def _get_ctx(): 

26 if _ctx is None: 

27 raise HTTPException(status_code=503, detail="서비스가 초기화되지 않았습니다.") 

28 return _ctx 

29 

30 

31def check_auth(request: Request): 

32 """로그인 여부를 확인하는 공통 함수.""" 

33 ctx = _get_ctx() 

34 expected_token = ctx.env.active_config.get("auth", {}).get("secret_key") 

35 token = request.cookies.get("access_token") 

36 

37 if token != expected_token: 

38 raise HTTPException(status_code=401, detail="Unauthorized") 

39 return True 

40 

41 

42def _serialize_response(resp): 

43 """ResCommonResponse를 JSON 직렬화 가능한 dict로 변환.""" 

44 if resp is None: 

45 return {"rt_cd": "999", "msg1": "응답 없음", "data": None} 

46 if hasattr(resp, 'to_dict'): 

47 return resp.to_dict() 

48 return {"rt_cd": str(resp.rt_cd), "msg1": str(resp.msg1), "data": resp.data} 

49 

50 

51def _serialize_list_items(items): 

52 """dataclass 리스트를 dict 리스트로 변환.""" 

53 result = [] 

54 for item in (items or []): 

55 if hasattr(item, 'to_dict'): 

56 result.append(item.to_dict()) 

57 elif isinstance(item, dict): 

58 result.append(item) 

59 else: 

60 result.append(str(item)) 

61 return result 

62 

63 

64# --- Pydantic 모델 --- 

65 

66class OrderRequest(BaseModel): 

67 code: str 

68 price: str 

69 qty: str 

70 side: str # "buy" or "sell" 

71 

72 

73class EnvironmentRequest(BaseModel): 

74 is_paper: bool 

75 

76 

77class ProgramTradingRequest(BaseModel): 

78 code: str 

79 

80 

81class ProgramTradingUnsubscribeRequest(BaseModel): 

82 code: str | None = None 

83 

84 

85class ProgramTradingDataModel(BaseModel): 

86 chartData: dict 

87 subscribedCodes: list 

88 codeNameMap: dict 

89 savedAt: str | None = None