diff --git a/backend/app/api/refresh.py b/backend/app/api/refresh.py index 438718f..43a6476 100644 --- a/backend/app/api/refresh.py +++ b/backend/app/api/refresh.py @@ -58,6 +58,7 @@ def reseed_symbols() -> dict: "updated": report.updated, "seed_marked": report.seed_marked, "markets": report.markets, + "static_fallback_used": report.static_fallback_used, } except Exception as e: # noqa: BLE001 # seed_symbols 내부에서 다 잡지만, 만에 하나 외부로 새는 예외 (logger 포매터 @@ -68,5 +69,6 @@ def reseed_symbols() -> dict: "updated": 0, "seed_marked": 0, "markets": {}, + "static_fallback_used": False, "error": repr(e)[:300], } diff --git a/backend/app/fetch/symbols_seed.py b/backend/app/fetch/symbols_seed.py index 9323106..1eb4fcd 100644 --- a/backend/app/fetch/symbols_seed.py +++ b/backend/app/fetch/symbols_seed.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from sqlalchemy import text from app.db.connection import get_engine +from app.seed.krx_static import KRX_STATIC_MAJORS from app.seed.seed_tickers import SEED_CODES, SEED_TICKERS logger = logging.getLogger(__name__) @@ -22,6 +23,7 @@ class SeedReport: updated: int seed_marked: int markets: dict[str, int] + static_fallback_used: bool = False def _fetch_market_listing(market: str) -> list[tuple[str, str]]: @@ -99,6 +101,21 @@ def seed_symbols() -> SeedReport: logger.error("seed_symbols: KRX %s fetch failed — skip market: %s", market, repr(e)[:300]) market_counts[market] = 0 + # 2b) KRX 가 0건만 줬으면 정적 스냅샷 (krx_static.KRX_STATIC_MAJORS) 으로 fallback. + # KRX 가 LOGOUT/JSONDecode 로 막혀도 주요 종목 검색은 동작하게 만든다. + # KRX 가 정상으로 돌아오면 다음 호출에서 all_rows 가 채워져 자동으로 덮어쓰여진다. + static_fallback_used = False + if not all_rows: + logger.warning( + "seed_symbols: KRX live fetch empty — fallback to static snapshot (%d majors)", + len(KRX_STATIC_MAJORS), + ) + all_rows = list(KRX_STATIC_MAJORS) + static_fallback_used = True + # market_counts 도 fallback 으로 채워 응답에 반영 + for _c, _n, m in KRX_STATIC_MAJORS: + market_counts[m] = market_counts.get(m, 0) + 1 + inserted = updated = 0 if all_rows: engine = get_engine() @@ -129,7 +146,13 @@ def seed_symbols() -> SeedReport: logger.error("seed_symbols: KRX bulk upsert failed (transaction rolled back): %s", repr(e)[:300]) logger.info( - "seed_symbols done: inserted=%d updated=%d seed_marked=%d markets=%s", - inserted, updated, seed_marked, market_counts, + "seed_symbols done: inserted=%d updated=%d seed_marked=%d markets=%s static_fallback=%s", + inserted, updated, seed_marked, market_counts, static_fallback_used, + ) + return SeedReport( + inserted=inserted, + updated=updated, + seed_marked=seed_marked, + markets=market_counts, + static_fallback_used=static_fallback_used, ) - return SeedReport(inserted=inserted, updated=updated, seed_marked=seed_marked, markets=market_counts) diff --git a/backend/app/seed/krx_static.py b/backend/app/seed/krx_static.py new file mode 100644 index 0000000..243cf9b --- /dev/null +++ b/backend/app/seed/krx_static.py @@ -0,0 +1,178 @@ +"""KRX 주요 종목 정적 스냅샷. + +용도: pykrx → KRX 실시간 호출이 LOGOUT/JSONDecodeError 로 실패할 때 +symbols 테이블 검색 universe 를 최소한 보장하기 위한 fallback. + +수록 범위: +- KOSPI/KOSDAQ 시가총액 상위 + 일반 사용자가 검색할 법한 친숙한 이름 위주. +- 종목코드(KRX 공시)와 종목명(현재 등록명)은 공개된 사실 정보. +- 전 종목은 KRX 실시간이 복구되면 seed_symbols() 가 자동 덮어쓴다. + +정확성 주의: +- 회사 합병/분할/사명변경/상장폐지가 잦아 일부 항목은 시간 지나면 stale. +- KRX 호출이 정상화되면 자동 upsert 로 덮어쓰므로 stale 항목은 자연 보정. +""" +from __future__ import annotations + +# (code, name, market) +KRX_STATIC_MAJORS: list[tuple[str, str, str]] = [ + # ---------------- KOSPI (시총 상위 + 익숙한 종목) ---------------- + ("005930", "삼성전자", "KOSPI"), + ("005935", "삼성전자우", "KOSPI"), + ("000660", "SK하이닉스", "KOSPI"), + ("373220", "LG에너지솔루션", "KOSPI"), + ("207940", "삼성바이오로직스", "KOSPI"), + ("005380", "현대차", "KOSPI"), + ("000270", "기아", "KOSPI"), + ("035420", "NAVER", "KOSPI"), + ("068270", "셀트리온", "KOSPI"), + ("005490", "POSCO홀딩스", "KOSPI"), + ("105560", "KB금융", "KOSPI"), + ("035720", "카카오", "KOSPI"), + ("028260", "삼성물산", "KOSPI"), + ("055550", "신한지주", "KOSPI"), + ("012330", "현대모비스", "KOSPI"), + ("086790", "하나금융지주", "KOSPI"), + ("051910", "LG화학", "KOSPI"), + ("006400", "삼성SDI", "KOSPI"), + ("003550", "LG", "KOSPI"), + ("015760", "한국전력공사", "KOSPI"), + ("032830", "삼성생명", "KOSPI"), + ("003670", "포스코퓨처엠", "KOSPI"), + ("017670", "SK텔레콤", "KOSPI"), + ("009150", "삼성전기", "KOSPI"), + ("011200", "HMM", "KOSPI"), + ("138040", "메리츠금융지주", "KOSPI"), + ("316140", "우리금융지주", "KOSPI"), + ("018260", "삼성에스디에스", "KOSPI"), + ("010130", "고려아연", "KOSPI"), + ("034730", "SK", "KOSPI"), + ("030200", "KT", "KOSPI"), + ("066570", "LG전자", "KOSPI"), + ("011070", "LG이노텍", "KOSPI"), + ("010950", "S-Oil", "KOSPI"), + ("047810", "한국항공우주", "KOSPI"), + ("024110", "기업은행", "KOSPI"), + ("138930", "BNK금융지주", "KOSPI"), + ("010140", "삼성중공업", "KOSPI"), + ("251270", "넷마블", "KOSPI"), + ("028050", "삼성E&A", "KOSPI"), + ("042660", "한화오션", "KOSPI"), + ("086280", "현대글로비스", "KOSPI"), + ("064350", "현대로템", "KOSPI"), + ("042700", "한미반도체", "KOSPI"), + ("267260", "HD현대일렉트릭", "KOSPI"), + ("329180", "HD현대중공업", "KOSPI"), + ("010620", "현대미포조선", "KOSPI"), + ("161390", "한국타이어앤테크놀로지", "KOSPI"), + ("097950", "CJ제일제당", "KOSPI"), + ("271560", "오리온", "KOSPI"), + ("175330", "JB금융지주", "KOSPI"), + ("139480", "이마트", "KOSPI"), + ("023530", "롯데쇼핑", "KOSPI"), + ("088350", "한화생명", "KOSPI"), + ("088980", "맥쿼리인프라", "KOSPI"), + ("004020", "현대제철", "KOSPI"), + ("047050", "포스코인터내셔널", "KOSPI"), + ("069960", "현대백화점", "KOSPI"), + ("003490", "대한항공", "KOSPI"), + ("035250", "강원랜드", "KOSPI"), + ("071050", "한국금융지주", "KOSPI"), + ("000810", "삼성화재", "KOSPI"), + ("011170", "롯데케미칼", "KOSPI"), + ("000720", "현대건설", "KOSPI"), + ("000150", "두산", "KOSPI"), + ("034020", "두산에너빌리티", "KOSPI"), + ("282330", "BGF리테일", "KOSPI"), + ("007070", "GS리테일", "KOSPI"), + ("078930", "GS", "KOSPI"), + ("011780", "금호석유", "KOSPI"), + ("002790", "아모레G", "KOSPI"), + ("090430", "아모레퍼시픽", "KOSPI"), + ("001040", "CJ", "KOSPI"), + ("005180", "빙그레", "KOSPI"), + ("049770", "동원F&B", "KOSPI"), + ("005300", "롯데칠성", "KOSPI"), + ("000080", "하이트진로", "KOSPI"), + ("004990", "롯데지주", "KOSPI"), + ("000100", "유한양행", "KOSPI"), + ("009540", "HD한국조선해양", "KOSPI"), + ("010060", "OCI홀딩스", "KOSPI"), + ("011790", "SKC", "KOSPI"), + ("004170", "신세계", "KOSPI"), + ("008770", "호텔신라", "KOSPI"), + ("002380", "KCC", "KOSPI"), + ("014820", "동원시스템즈", "KOSPI"), + ("005830", "DB손해보험", "KOSPI"), + ("089860", "롯데렌탈", "KOSPI"), + ("009830", "한화솔루션", "KOSPI"), + ("006650", "대한유화", "KOSPI"), + ("000240", "한국앤컴퍼니", "KOSPI"), + ("026960", "동서", "KOSPI"), + ("001450", "현대해상", "KOSPI"), + ("003410", "쌍용C&E", "KOSPI"), + ("003090", "대웅", "KOSPI"), + ("069620", "대웅제약", "KOSPI"), + ("128940", "한미약품", "KOSPI"), + ("004000", "롯데정밀화학", "KOSPI"), + ("000120", "CJ대한통운", "KOSPI"), + ("005880", "대한해운", "KOSPI"), + ("001230", "동국홀딩스", "KOSPI"), + ("000990", "DB하이텍", "KOSPI"), + ("036570", "엔씨소프트", "KOSPI"), + ("267250", "HD현대", "KOSPI"), + + # ---------------- KOSDAQ (시총/거래대금/이름 인지도) ---------------- + ("247540", "에코프로비엠", "KOSDAQ"), + ("086520", "에코프로", "KOSDAQ"), + ("035760", "CJ ENM", "KOSDAQ"), + ("058470", "리노공업", "KOSDAQ"), + ("196170", "알테오젠", "KOSDAQ"), + ("091990", "셀트리온헬스케어", "KOSDAQ"), + ("028300", "HLB", "KOSDAQ"), + ("357780", "솔브레인", "KOSDAQ"), + ("263750", "펄어비스", "KOSDAQ"), + ("293490", "카카오게임즈", "KOSDAQ"), + ("145020", "휴젤", "KOSDAQ"), + ("277810", "레인보우로보틱스", "KOSDAQ"), + ("240810", "원익IPS", "KOSDAQ"), + ("357180", "LX세미콘", "KOSDAQ"), + ("376300", "디어유", "KOSDAQ"), + ("095610", "테스", "KOSDAQ"), + ("067160", "아프리카TV", "KOSDAQ"), + ("067310", "하나마이크론", "KOSDAQ"), + ("388790", "케이엠더블유", "KOSDAQ"), + ("089030", "테크윙", "KOSDAQ"), + ("086900", "메디톡스", "KOSDAQ"), + ("035900", "JYP Ent.", "KOSDAQ"), + ("041510", "에스엠", "KOSDAQ"), + ("122870", "와이지엔터테인먼트", "KOSDAQ"), + ("352820", "하이브", "KOSDAQ"), + ("039030", "이오테크닉스", "KOSDAQ"), + ("066970", "엘앤에프", "KOSDAQ"), + ("112040", "위메이드", "KOSDAQ"), + ("194700", "네오위즈", "KOSDAQ"), + ("095700", "제넥신", "KOSDAQ"), + ("214150", "클래시스", "KOSDAQ"), + ("121600", "나노신소재", "KOSDAQ"), + ("064550", "바이오니아", "KOSDAQ"), + ("304100", "솔트룩스", "KOSDAQ"), + ("078340", "컴투스", "KOSDAQ"), + ("226330", "신테카바이오", "KOSDAQ"), + ("215000", "골프존", "KOSDAQ"), + ("950140", "잉글우드랩", "KOSDAQ"), + ("328130", "루닛", "KOSDAQ"), + ("000250", "삼천당제약", "KOSDAQ"), + ("237690", "에스티팜", "KOSDAQ"), + ("196300", "쇼박스", "KOSDAQ"), + ("215600", "신라젠", "KOSDAQ"), + ("357580", "엠로", "KOSDAQ"), + ("403870", "HPSP", "KOSDAQ"), + ("078160", "메디포스트", "KOSDAQ"), + ("141080", "리가켐바이오", "KOSDAQ"), + ("306200", "세아메카닉스", "KOSDAQ"), + ("213420", "덕산네오룩스", "KOSDAQ"), + ("085660", "차바이오텍", "KOSDAQ"), + ("950130", "엑세스바이오", "KOSDAQ"), + ("950170", "JTC", "KOSDAQ"), +]