refactor: 전체 코드베이스 감사 기반 리팩토링 — 버그 수정, 데드코드 제거, 보안 강화

P0 버그 수정:
- stock-lab: trade 엔드포인트 NameError 수정 (resp 미정의)
- deployer: 동시 배포 시 HTTP 200 → 503 반환

P1 데드코드 제거:
- stock-lab: fetch_overseas_news(), get_broker_cash() 제거
- blog-lab: 미사용 urlparse import 제거
- lotto-lab: 중복 inline import json 7곳 제거

P2 성능/효율 개선:
- lotto-lab: 가중 샘플링 3중 복사 → utils.weighted_sample_6() 통합
- lotto-lab: DB 인덱스 3개 추가 (recommendations, purchase_history)
- stock-lab: Pydantic .dict() → .model_dump() 호환
- blog-lab: 페이지네이션 상한(le=100) 추가

P3 보안/인프라:
- nginx: X-Frame-Options, X-Content-Type-Options, Referrer-Policy 헤더 추가
- docker-compose: travel-proxy CORS 와일드카드 → localhost 전용
- Dockerfile: music-lab, blog-lab, realestate-lab에 PYTHONUNBUFFERED 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 04:10:14 +09:00
parent 9d5583935d
commit 535ffea45a
15 changed files with 84 additions and 150 deletions

View File

@@ -183,12 +183,6 @@ def get_all_broker_cash() -> List[Dict[str, Any]]:
return [dict(r) for r in rows]
def get_broker_cash(broker: str) -> Dict[str, Any] | None:
with _conn() as conn:
row = conn.execute("SELECT * FROM broker_cash WHERE broker = ?", (broker,)).fetchone()
return dict(row) if row else None
def delete_broker_cash(broker: str) -> bool:
with _conn() as conn:
cur = conn.execute("DELETE FROM broker_cash WHERE broker = ?", (broker,))

View File

@@ -17,11 +17,11 @@ from .db import (
init_db, save_articles, get_latest_articles,
add_portfolio_item, get_all_portfolio, get_portfolio_item,
update_portfolio_item, delete_portfolio_item,
upsert_broker_cash, get_all_broker_cash, get_broker_cash, delete_broker_cash,
upsert_broker_cash, get_all_broker_cash, delete_broker_cash,
upsert_asset_snapshot, get_asset_snapshots,
add_sell_history, get_sell_history, update_sell_history, delete_sell_history,
)
from .scraper import fetch_market_news, fetch_major_indices, fetch_overseas_news
from .scraper import fetch_market_news, fetch_major_indices
from .price_fetcher import get_current_prices
app = FastAPI()
@@ -118,17 +118,11 @@ def on_startup():
def run_scraping_job():
logger.info("뉴스 스크래핑 시작")
# 1. 국내
articles_kr = fetch_market_news()
count_kr = save_articles(articles_kr)
# 2. 해외 (임시 차단)
# articles_world = fetch_overseas_news()
# count_world = save_articles(articles_world)
count_world = 0
logger.info(f"스크래핑 완료: 국내 {count_kr}건, 해외 {count_world}")
logger.info(f"스크래핑 완료: 국내 {count_kr}")
@app.get("/health")
def health():
@@ -156,14 +150,16 @@ def trigger_scrap():
def get_balance():
"""계좌 잔고 조회 (Windows AI Server Proxy)"""
logger.info(f"Requesting Balance from {WINDOWS_AI_SERVER_URL}")
resp = None
try:
resp = requests.get(f"{WINDOWS_AI_SERVER_URL}/trade/balance", timeout=5)
if resp.status_code != 200:
logger.error(f"Balance Error: {resp.status_code}")
return JSONResponse(status_code=resp.status_code, content=resp.json())
return resp.json()
except requests.JSONDecodeError:
return JSONResponse(status_code=resp.status_code, content={"error": f"Upstream error {resp.status_code}"})
except ValueError:
status = resp.status_code if resp is not None else 502
return JSONResponse(status_code=status, content={"error": f"Upstream error {status}"})
except Exception as e:
logger.error(f"Balance Connection Failed: {e}")
return JSONResponse(status_code=500, content={"error": "Connection Failed"})
@@ -179,14 +175,16 @@ class OrderRequest(BaseModel):
def order_stock(req: OrderRequest):
"""주식 매수/매도 주문 (Windows AI Server Proxy)"""
logger.info(f"Order Request: {req.action} {req.ticker} x{req.quantity}")
resp = None
try:
resp = requests.post(f"{WINDOWS_AI_SERVER_URL}/trade/order", json=req.dict(), timeout=10)
resp = requests.post(f"{WINDOWS_AI_SERVER_URL}/trade/order", json=req.model_dump(), timeout=10)
if resp.status_code != 200:
logger.error(f"Order Error: {resp.status_code}")
return JSONResponse(status_code=resp.status_code, content=resp.json())
return resp.json()
except requests.JSONDecodeError:
return JSONResponse(status_code=resp.status_code, content={"error": f"Upstream error {resp.status_code}"})
except ValueError:
status = resp.status_code if resp is not None else 502
return JSONResponse(status_code=status, content={"error": f"Upstream error {status}"})
except Exception as e:
logger.error(f"Order Connection Failed: {e}")
return JSONResponse(status_code=500, content={"error": "Connection Failed"})
@@ -368,7 +366,7 @@ def update_portfolio(item_id: int, req: PortfolioUpdateRequest):
"""포트폴리오 종목 수정"""
if get_portfolio_item(item_id) is None:
return JSONResponse(status_code=404, content={"error": "Item not found"})
update_portfolio_item(item_id, **req.dict())
update_portfolio_item(item_id, **req.model_dump())
return {"ok": True}

View File

@@ -79,59 +79,6 @@ def fetch_market_news() -> List[Dict[str, str]]:
logger.error(f"국내 뉴스 스크래핑 실패: {e}")
return []
def fetch_overseas_news() -> List[Dict[str, str]]:
"""
네이버 금융 해외증시 뉴스 크롤링 (모바일 API 사용)
"""
api_url = "https://api.stock.naver.com/news/overseas/mainnews"
try:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
}
resp = requests.get(api_url, headers=headers, timeout=10)
resp.raise_for_status()
data = resp.json()
if isinstance(data, list):
items = data
else:
items = data.get("result", [])
articles = []
for item in items:
# API 키 매핑 (subject/title/tit, summary/subContent/sub_tit 등)
title = item.get("subject") or item.get("title") or item.get("tit") or ""
summary = item.get("summary") or item.get("subContent") or item.get("sub_tit") or ""
press = item.get("officeName") or item.get("office_name") or item.get("cp_name") or ""
# 날짜 포맷팅 (20260126123000 -> 2026-01-26 12:30:00)
raw_dt = str(item.get("dt", ""))
if len(raw_dt) == 14:
date = f"{raw_dt[:4]}-{raw_dt[4:6]}-{raw_dt[6:8]} {raw_dt[8:10]}:{raw_dt[10:12]}:{raw_dt[12:]}"
else:
date = raw_dt
# 링크 생성
aid = item.get("articleId")
oid = item.get("officeId")
link = f"https://m.stock.naver.com/worldstock/news/read/{oid}/{aid}"
articles.append({
"title": title,
"link": link,
"summary": summary,
"press": press,
"date": date,
"crawled_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"category": "overseas"
})
return articles
except Exception as e:
logger.error(f"해외 뉴스 스크래핑 실패: {e}")
return []
def fetch_major_indices() -> Dict[str, Any]:
"""
KOSPI, KOSDAQ, KOSPI200 등 주요 지표 (네이버 금융 홈)