From 4c8c879302af9561ffeb3103ad3295ed04574231 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Tue, 23 Jun 2026 01:21:30 +0900 Subject: [PATCH] =?UTF-8?q?WBS-9=20=EC=A7=84=ED=96=89:=20API=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20DB=20=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=EB=A7=88=20=EC=A0=95=EA=B7=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - snapshot_admin_store_v1.py: summarize_workspace에서 account_snapshot의 captured_at 사용 (updated_at 대신) - account_snapshot 테이블: 올바른 스키마 재정의 (ordinal PK, row_json, 핵심필드, updated_at) - settings 테이블: 올바른 스키마 재정의 (ordinal PK, key, value_json, note, updated_at) - initialize_snapshot_admin_db.py: XLSX에서 settings, account_snapshot을 올바른 스키마로 로드 - load_from_xlsx_correct.py: account_snapshot을 특별 처리해서 스키마 보존 - /api/settings/save: 정상 작동 (200 응답) - build_ui_state: load_collection_dashboard_state 예외 처리 추가 (진행 중) 데이터 현황: - kis_data_collection.db: 1 테이블 (data_feed), 25행 - snapshot_admin.db: 27 테이블, 7,501행 * settings: 32행 (올바른 스키마) * account_snapshot: 44행 (올바른 스키마) 남은 작업: - /api/state 크래시 원인 진단 및 수정 - /api/export 데이터 검증 - 웹 UI 개선 (백오피스 수준) - T+20 모니터링 활성화 - CI/CD 백업 기능 Co-Authored-By: Claude Haiku 4.5 --- .gitignore | 2 + src/quant_engine/kis_data_collection.db | Bin 61440 -> 61440 bytes src/quant_engine/snapshot_admin.db | Bin 1490944 -> 1499136 bytes src/quant_engine/snapshot_admin_server_v1.py | 5 +- src/quant_engine/snapshot_admin_store_v1.py | 6 +- tools/check_schema.py | 27 ++++ tools/diagnose_api_error.py | 95 ++++++++++++ tools/fix_account_snapshot_schema.py | 118 ++++++++++++++ tools/fix_account_snapshot_v2.py | 110 +++++++++++++ tools/fix_settings_schema_v2.py | 83 ++++++++++ tools/initialize_snapshot_admin_db.py | 153 +++++++++++++++++++ tools/load_from_xlsx_correct.py | 54 ++++++- tools/test_api_components.py | 62 ++++++++ tools/test_api_settings_save.py | 57 +++++++ tools/test_build_ui_state.py | 131 ++++++++++++++++ tools/test_build_ui_state_simple.py | 93 +++++++++++ tools/test_import.py | 12 ++ tools/test_remaining_apis.py | 79 ++++++++++ tools/verify_data_load.py | 55 +++++++ 19 files changed, 1136 insertions(+), 6 deletions(-) create mode 100644 tools/check_schema.py create mode 100644 tools/diagnose_api_error.py create mode 100644 tools/fix_account_snapshot_schema.py create mode 100644 tools/fix_account_snapshot_v2.py create mode 100644 tools/fix_settings_schema_v2.py create mode 100644 tools/initialize_snapshot_admin_db.py create mode 100644 tools/test_api_components.py create mode 100644 tools/test_api_settings_save.py create mode 100644 tools/test_build_ui_state.py create mode 100644 tools/test_build_ui_state_simple.py create mode 100644 tools/test_import.py create mode 100644 tools/test_remaining_apis.py create mode 100644 tools/verify_data_load.py diff --git a/.gitignore b/.gitignore index ecd802b..3055c58 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ node_modules/ # Claude 세션 캐시 (자동메모리 제외) .claude/projects/ +*.db-shm +*.db-wal diff --git a/src/quant_engine/kis_data_collection.db b/src/quant_engine/kis_data_collection.db index f2c5fce554966079c2b1fbe03c5b33770730c333..befa4687f0ed3c54780d946afa30af7d41d95635 100644 GIT binary patch delta 180 zcmZp8z})bFd4jYcF9QPuKM->OF%u9AP1G@F( z32r_h!Nm!)rfCdHp=HH~i0u+7lhaUi$G#tPH delta 1429 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 KANpj_Z2UiyFXFcV delta 5881 zcma)A3v?6Lnbwswl13VPMxz(9FqSdKHpXH`Z}F6|4fya9ws|Bxgs_F}*jS1zhqw*F zvI%UHkT{2gOLk8i=KgKWPZ}O^5)vR7d;dEo zgcSF5W&fXa@16VI|8-~nJ3Yf&dwRC^JB+&(3e>JpD4qaOfk;oy_txNctADPYetp?=!Jb=c&nSgws2GG=-1T^>V0JQWr09t#i0d2ix z^%=c{&)#bX%5r7*t0ON}QZW2W%$n6E`PoLNheZeQpfU^%1>xvE&&(3+A%qo7Y6L9Wh<$xuR5)DfqbpV$Ah`4dyj|_n2 z|4;=u|IiNsD-IFQxDK`fE;v{OC_J(iP<+G$=srOBJqHNCw~z4q?7d_Ge=k`eAiH2; zhAfc3E<3`ixk2?X3n3a}m7Js*U?6^oZct5BN!2qEt1m`V+_Dwrpzl-!`k zsRH0s+;I*0F-E#ZiDDy4nv>XJVn zAvCH2QG-w*8VQYRCtp80qEa7M!&2%V;Li^0)ZBjHQ-aKbY|o$iRusVZryxO)*&rl{ zb3lqgNcxaeE(IwAnFm55Hy@+|#09bdgybsG)eS-t!VBU9@q+|FNJcXtHD3XI4RRIa z8pw5!Z$NH%*;U>5>A5!j0gL7X7q2Soe=dyV)8<>6IljYVZnQ4ls));2^V8`rE^URkv| zQ)+b-iiP$C#fp5J&6aPmm@EpD!h|cnM4KBtq8JLeLjkYg4hDQ)cSvvxu7FSUheD!D z5CxaV=Wzvr6aK5#uUk=Dx3;D_T3KBkUB50+%uoyrcDud6W|}g(Vv3>QE41_;H)!Y$xJ8cu)`fz8ug@(6J#IhI z&?kzZBGJ(2bBTY${8_xbMuBX;e->-TtkZXoc6e4&8L=M4lyUY8i~ z1Yr$VC@8r6KCf#!3q+&LzIw{JtmXR_w115@L_~K`@Qdz1P!QdIk+@g@-$Kwpaah$w z)`kDO;#SPb)VJcsewz`uoKtueRc{cz5G>Ib0s^l`aDyL!$wP93(EB}q(l zbY^_+DufPv;PwliP|)R@F8J15nPKJh4E5L0I)BLP1?7BhuM6UJcltPUWQJS8x7$d@ zvkDfETtjsm#Go%2@OmKq1i>SOAo^}ESSPqWArA?M7kuZ3Z^5^svUUw5`|`S)%GK*P zHe|{fnYIiRbHK+wwULZCc^x(E5Cb8f2R4v97zAR-B*+}_T?q1D5CZ~P%|$}v@w?^y zZHl?_#^YO2M6 zi^L|AuO|eya=UGjojl8LeQd=*aQvKZ9{%7P~3%x!OeuAb{(ivwPs^|?WP*h z=MvmNl}F1|#nVx(yMeZcVKakGQ+O8(1O{9oWK24lr2ts&w8%y*`rvI)$T*IK#d0iU zkcqyt3$~yLn=(_Zm1(Mg#z7vw-`eG{81en@0L}4nw0yDX1s{`qmA|H_Zu!S05^d&b z2#tNiUu)w4ZD!p>=@6ubTae*5_Bmt)IF24}( z5`*k&LS>oOyU_`>Ei49M=Yc8&r)3EOOP-3bn98P8==1kLp~6Yz35Os71z`&BqH?=k zArYt;rA%f2YbxSlpfcfs&lnqil~tDS7kvS8SCH|0ZgP1b2YWXmM^S!}-Qp-JDJm-| zDJw533;4<^%ZePu4o6+QF_CJDR>#uuhE%L69<8meZ*A^~wNG`mHnzo6>tcK2m5q&w zu8wp~M>^FVtxw0A_=X!yTt zR5sMzF|;Ys-nAzlt&62w6H#CFiuT0bXjLlS)S9lVuNA$wDe(SFwB?9XW6oWkipSa# zUFm2&m=He_(c~?xkEgqm(du|(YiDbsqvpZZbhIuW>r8Y+I@UxZjpl!iNbNYLi%9AbQtS-quI4JyZhZZuu5A6inFru`eotl_;Fio5z|EP> zfSWR;Uf7r+6+=S?>KU{>1Ct=9T_L!h773;)@Q7M5q$s4x;bbCKKin5 zwqfxu=vo*9Sc2bx4#ylQc~KFmRSHS5G7HKolt=2J3&kfQG?z`%&0171wd|#j>ExPE z$&!Ky-6t_S=}T;7E7V!YVamz9rqDv|l+&TlYG2R>C`QRZGoW|{{@`P%(d|<{M1I}* z>FcZ8CAm7R|Bh6T?2F-(AJY}+S?Pn1>GMj}fzqAW{Aaox|LTM)S6cgLdJLf`UiJxH zj*jBCPw1su;|{H@q(q5NeL@#16_4Y?P1bGFg1^vDD$#a4e1Z0(kaYC|-J?X?aQ9_; zE^5xITNMWuh1#9eVX4; zS;c-7K+6y|j+o~jafX+x9`!ck{m1Ac^lRzGV>C~p-Nf58Qe~FL@uenfA%5mT`p3#2 zHsOG*loS3&5qG}PN_Kg3ZwsmMK4N|te)Y}ze@P>3h>69t3n0V`zSKrX}J2KYpymdwX zEGlGFDDbHdbynIV6y#G&NK<^zI-|tnn>mvTK(j5;nQV=A#`m;F_r_bBThh^FW7_X4 zbWl|+TwM9zI)^Uk%%hf(sn9ZU9f{PQSi9)%vTIgmMCcuR8v){%2`t_F_qLvU`_jLI)4hTw3n9zf(^-;V%#2qd9XvlEdcegxX; zFu6#8izJN`dL=anzhBY7&llBW+>7c_kP+^*dYD`>0xvKQsmEaM7&icF>R@gU4R11r zj9fMhH$ZsG8ONj9BtM}>f;5@t-&CUo(wp6UP=n@X1zyQ1Q3-y2$Y{j_KjF25=a29R z3vci<@pJq5GTeWjPh!?#vP+*m#hY_cDIS<<+JTon%$xE2XZcAy{9E3Rh2v%&?m5O6 z;>i#B`{hw-@eBN$Ij9UzWO)HcG^VL;9Da?zg2#T%TX5tR;CG$jA0vIA7Yhs8g@vhpI!e#1YEM<3>&!XvNqWzz6jzA6`$wxNmQ;8HP*UQv@0xA6=OSNtRBzgg!#@Xe zd#>;|asU7F4jjJBmtt6`?e@UKIQcfL$Nh}ohtI!jrsb*M`9{7@0)jpRa^b=mrUdbz z1MmNZNrw;rr_qjw_wl;h*L(3t*ZIe%6c11EF+BV$SpU0=!++r)z+q$>oLY5+Gj-oF z+H;lfzGGA-_^iOh;aS&t1}7Ht+M_yCp#&ZuvS)jE6^BGgy_1J@j6kp@Wtg8}vrf~! zdgPY2&NID4q2jDyQtyQKMJAl<|n8VVYBmSKLfAGddx|9Oj$@)S|G2f zA>Imr=GjC`_Tcu6)mR~Sv8!Tf%&OvGzhNyn|s!!*O&J}o2wdns% z*Q-rI6F#~h95IHs8Z<2$(o)?0EXVLHKc(uWI;5BXL{muBo3lr%=%-nP+VRq-=mPYP zwBsqLUr-y)9HC3lIsDTjbazw6e4o~qpRdexr(?U?Ob@V{ns dict[str, Any]: snapshot_errors = validate_account_snapshot_rows(account_rows) suggestions = build_validation_suggestions(settings_rows, account_rows) autofix_actions = build_safe_autofix_actions(settings_rows, account_rows) - collection = load_collection_dashboard_state(KIS_COLLECTION_DB, KIS_COLLECTION_REPORT) + try: + collection = load_collection_dashboard_state(KIS_COLLECTION_DB, KIS_COLLECTION_REPORT) + except Exception: + collection = {} return { "version": { "app": SNAPSHOT_ADMIN_VERSION, diff --git a/src/quant_engine/snapshot_admin_store_v1.py b/src/quant_engine/snapshot_admin_store_v1.py index 4375c42..acd105c 100644 --- a/src/quant_engine/snapshot_admin_store_v1.py +++ b/src/quant_engine/snapshot_admin_store_v1.py @@ -723,11 +723,11 @@ def summarize_workspace(db_path: Path | str | None = None) -> dict[str, Any]: snapshot_count = conn.execute(f"SELECT COUNT(*) FROM {SNAPSHOT_TABLE}").fetchone()[0] latest_update = conn.execute( f""" - SELECT MAX(updated_at) + SELECT MAX(latest_ts) FROM ( - SELECT updated_at FROM {SETTINGS_TABLE} + SELECT updated_at as latest_ts FROM {SETTINGS_TABLE} UNION ALL - SELECT updated_at FROM {SNAPSHOT_TABLE} + SELECT captured_at FROM {SNAPSHOT_TABLE} ) """ ).fetchone()[0] diff --git a/tools/check_schema.py b/tools/check_schema.py new file mode 100644 index 0000000..468c94b --- /dev/null +++ b/tools/check_schema.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import sqlite3 +from pathlib import Path + +db_path = Path('src/quant_engine/snapshot_admin.db') +conn = sqlite3.connect(db_path) +cursor = conn.cursor() + +# 테이블 목록 +cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") +tables = [row[0] for row in cursor.fetchall()] +print(f"전체 테이블: {tables}\n") + +# 각 테이블 스키마 +for table_name in ['account_snapshot', 'snapshot', 'settings', 'performance', 'positions']: + try: + cursor.execute(f"PRAGMA table_info({table_name})") + cols = cursor.fetchall() + if cols: + print(f"{table_name} 컬럼:") + for col in cols: + print(f" {col[1]} ({col[2]})") + print() + except: + pass + +conn.close() diff --git a/tools/diagnose_api_error.py b/tools/diagnose_api_error.py new file mode 100644 index 0000000..e89d041 --- /dev/null +++ b/tools/diagnose_api_error.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +/api/settings/save 500 에러 진단 +replace_settings 함수 직접 테스트 +""" + +import sys +sys.path.insert(0, 'src/quant_engine') + +from snapshot_admin_store_v1 import ( + open_connection, + replace_settings, + load_settings_rows, + validate_settings_rows, +) +from pathlib import Path + +def diagnose_settings_save(): + """Settings 저장 함수 직접 테스트""" + + db_path = Path('src/quant_engine/snapshot_admin.db') + + print("="*80) + print("Settings 저장 함수 진단") + print("="*80) + + # 테스트 데이터 + test_rows = [ + { + "ordinal": 5, + "key": "total_asset_krw", + "value": "450000000", + "note": "테스트 수정" + } + ] + + print("\n[1단계] 검증 테스트") + try: + errors = validate_settings_rows(test_rows) + if errors: + print(f" [FAIL] 검증 오류: {errors}") + return + print(f" [OK] 검증 통과") + except Exception as e: + print(f" [ERROR] 검증 함수 실패: {e}") + return + + print("\n[2단계] replace_settings 함수 테스트") + try: + with open_connection(db_path) as conn: + replace_settings(conn, test_rows) + print(f" [OK] replace_settings 성공") + except Exception as e: + print(f" [FAIL] replace_settings 오류") + print(f" 오류 타입: {type(e).__name__}") + print(f" 오류 메시지: {e}") + import traceback + traceback.print_exc() + return + + print("\n[3단계] 저장 결과 확인") + try: + with open_connection(db_path) as conn: + rows = load_settings_rows_from_conn(conn) + for row in rows: + if row['key'] == 'total_asset_krw': + print(f" [OK] {row['key']} = {row['value']}") + except Exception as e: + print(f" [ERROR] 조회 실패: {e}") + + print("\n[완료] 진단 끝") + +# Helper 함수 +def load_settings_rows_from_conn(conn): + """직접 로드""" + import sqlite3 + import json + + rows = conn.execute( + "SELECT ordinal, key, value_json, note, updated_at FROM settings ORDER BY ordinal ASC" + ).fetchall() + + return [ + { + "ordinal": int(row[0]), + "key": row[1], + "value": json.loads(row[2]), + "note": row[3], + "updated_at": row[4], + } + for row in rows + ] + +if __name__ == "__main__": + diagnose_settings_save() diff --git a/tools/fix_account_snapshot_schema.py b/tools/fix_account_snapshot_schema.py new file mode 100644 index 0000000..9542f60 --- /dev/null +++ b/tools/fix_account_snapshot_schema.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +account_snapshot 테이블 스키마 수정 +last_updated -> updated_at +""" + +import sqlite3 +from pathlib import Path + +def fix_account_snapshot_schema(): + """account_snapshot 테이블 스키마 수정""" + + db_path = Path('src/quant_engine/snapshot_admin.db') + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 현재 컬럼 확인 + cursor.execute("PRAGMA table_info(account_snapshot)") + current_cols = {col[1]: col[2] for col in cursor.fetchall()} + print(f"현재 컬럼: {list(current_cols.keys())}") + + # 데이터 백업 + cursor.execute("SELECT * FROM account_snapshot LIMIT 1") + sample = cursor.fetchone() + print(f"\n샘플 행 컬럼: {sample.keys() if sample else 'NO DATA'}") + + # account_snapshot 데이터 백업 + cursor.execute("SELECT * FROM account_snapshot") + backup = cursor.fetchall() + print(f"백업 행 수: {len(backup)}") + + # 기존 테이블 삭제 + cursor.execute("DROP TABLE IF EXISTS account_snapshot") + + # 올바른 스키마로 생성 + cursor.execute(""" + CREATE TABLE account_snapshot ( + ordinal INTEGER NOT NULL, + row_json TEXT NOT NULL, + captured_at TEXT NOT NULL DEFAULT '', + account TEXT NOT NULL DEFAULT '', + account_type TEXT NOT NULL DEFAULT '', + ticker TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL DEFAULT '', + parse_status TEXT NOT NULL DEFAULT '', + user_confirmed TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL + ) + """) + + cursor.execute("CREATE INDEX IF NOT EXISTS idx_account_snapshot_captured_at ON account_snapshot(captured_at)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_account_snapshot_ticker ON account_snapshot(ticker)") + + print("\n새 스키마 생성:") + print(" ordinal INTEGER NOT NULL") + print(" row_json TEXT NOT NULL") + print(" captured_at TEXT NOT NULL DEFAULT ''") + print(" account TEXT NOT NULL DEFAULT ''") + print(" account_type TEXT NOT NULL DEFAULT ''") + print(" ticker TEXT NOT NULL DEFAULT ''") + print(" name TEXT NOT NULL DEFAULT ''") + print(" parse_status TEXT NOT NULL DEFAULT ''") + print(" user_confirmed TEXT NOT NULL DEFAULT ''") + print(" updated_at TEXT NOT NULL") + + # 데이터 복원 + if backup: + print(f"\n데이터 복원 중: {len(backup)}개 행") + + for row_dict in backup: + # row_dict는 sqlite3.Row 타입 + values = [] + for col_name in [ + 'ordinal', 'row_json', 'captured_at', 'account', 'account_type', + 'ticker', 'name', 'parse_status', 'user_confirmed' + ]: + if col_name in row_dict.keys(): + values.append(row_dict[col_name]) + else: + values.append(None) + + # updated_at: last_updated 또는 captured_at 사용 + if 'last_updated' in row_dict.keys() and row_dict['last_updated']: + values.append(str(row_dict['last_updated'])) + elif 'captured_at' in row_dict.keys() and row_dict['captured_at']: + values.append(str(row_dict['captured_at'])) + else: + values.append('') + + cursor.execute(""" + INSERT INTO account_snapshot ( + ordinal, row_json, captured_at, account, account_type, ticker, name, + parse_status, user_confirmed, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, values) + + conn.commit() + print(f"[OK] {len(backup)}개 행 복원") + else: + conn.commit() + print("[OK] 테이블 생성 (데이터 없음)") + + # 검증 + cursor.execute("SELECT COUNT(*) FROM account_snapshot") + count = cursor.fetchone()[0] + print(f"\n검증: {count}개 행") + + cursor.execute("PRAGMA table_info(account_snapshot)") + new_cols = {col[1]: col[2] for col in cursor.fetchall()} + print(f"새 컬럼: {list(new_cols.keys())}") + + conn.close() + + print("\n[OK] account_snapshot 테이블 스키마 수정 완료") + +if __name__ == "__main__": + fix_account_snapshot_schema() diff --git a/tools/fix_account_snapshot_v2.py b/tools/fix_account_snapshot_v2.py new file mode 100644 index 0000000..78b8d25 --- /dev/null +++ b/tools/fix_account_snapshot_v2.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +account_snapshot 테이블을 올바른 스키마로 마이그레이션 +현재: captured_at, account, ticker, ... (XLSX 스키마) +목표: ordinal (PK), row_json, captured_at, account, ... , updated_at +""" + +import sqlite3 +import json +from pathlib import Path +from datetime import datetime + +def fix_account_snapshot_v2(): + """account_snapshot 테이블 마이그레이션""" + + db_path = Path('src/quant_engine/snapshot_admin.db') + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 현재 데이터 백업 + cursor.execute("SELECT * FROM account_snapshot") + old_rows = cursor.fetchall() + print(f"현재 account_snapshot: {len(old_rows)}개 행") + + # 기존 컬럼 확인 + cursor.execute("PRAGMA table_info(account_snapshot)") + old_cols = {col[1]: col[2] for col in cursor.fetchall()} + print(f"현재 컬럼: {len(old_cols)}개") + + # 기존 테이블 백업 + cursor.execute("ALTER TABLE account_snapshot RENAME TO account_snapshot_old") + + # 올바른 스키마로 생성 + cursor.execute(""" + CREATE TABLE account_snapshot ( + ordinal INTEGER PRIMARY KEY, + row_json TEXT NOT NULL, + captured_at TEXT NOT NULL DEFAULT '', + account TEXT NOT NULL DEFAULT '', + account_type TEXT NOT NULL DEFAULT '', + ticker TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL DEFAULT '', + parse_status TEXT NOT NULL DEFAULT '', + user_confirmed TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL + ) + """) + + cursor.execute("CREATE INDEX IF NOT EXISTS idx_account_snapshot_captured_at ON account_snapshot(captured_at)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_account_snapshot_ticker ON account_snapshot(ticker)") + + print("\n새 스키마 생성:") + print(" ordinal INTEGER PRIMARY KEY") + print(" row_json TEXT NOT NULL") + print(" captured_at TEXT NOT NULL DEFAULT ''") + print(" ... (8개 핵심 컬럼) ...") + print(" updated_at TEXT NOT NULL") + + # 데이터 마이그레이션 + timestamp = datetime.now().isoformat() + + print(f"\n데이터 마이그레이션 중: {len(old_rows)}개 행") + + for ordinal, old_row in enumerate(old_rows, start=1): + # sqlite3.Row를 dict로 변환 + row_dict = dict(old_row) + + # 필요한 필드 추출 + captured_at = str(row_dict.get('captured_at') or '') + account = str(row_dict.get('account') or '') + account_type = str(row_dict.get('account_type') or '') + ticker = str(row_dict.get('ticker') or '') + name = str(row_dict.get('name') or '') + parse_status = str(row_dict.get('parse_status') or '') + user_confirmed = str(row_dict.get('user_confirmed') or '') + + # 전체 행을 row_json으로 저장 + row_json = json.dumps(row_dict, default=str, ensure_ascii=False) + + cursor.execute(""" + INSERT INTO account_snapshot ( + ordinal, row_json, captured_at, account, account_type, ticker, name, + parse_status, user_confirmed, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + ordinal, row_json, captured_at, account, account_type, ticker, name, + parse_status, user_confirmed, timestamp + )) + + conn.commit() + + # 검증 + cursor.execute("SELECT COUNT(*) FROM account_snapshot") + count = cursor.fetchone()[0] + print(f"마이그레이션된 account_snapshot: {count}개 행") + + cursor.execute("PRAGMA table_info(account_snapshot)") + new_cols = {col[1]: col[2] for col in cursor.fetchall()} + print(f"새 컬럼: {list(new_cols.keys())}") + + # 이전 테이블 삭제 + cursor.execute("DROP TABLE account_snapshot_old") + + conn.close() + + print(f"\n[OK] account_snapshot 테이블 마이그레이션 완료") + +if __name__ == "__main__": + fix_account_snapshot_v2() diff --git a/tools/fix_settings_schema_v2.py b/tools/fix_settings_schema_v2.py new file mode 100644 index 0000000..41838b5 --- /dev/null +++ b/tools/fix_settings_schema_v2.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +settings 테이블을 올바른 스키마로 수정 +현재: key, value +목표: ordinal (PK), key (NOT NULL), value_json (JSON), note, updated_at +""" + +import sqlite3 +import json +from pathlib import Path +from datetime import datetime + +def fix_settings_schema_v2(): + """settings 테이블 스키마 수정""" + + db_path = Path('src/quant_engine/snapshot_admin.db') + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 현재 데이터 백업 + cursor.execute("SELECT key, value FROM settings") + old_rows = cursor.fetchall() + print(f"현재 settings: {len(old_rows)}개 행") + + # 기존 테이블 삭제 + cursor.execute("DROP TABLE IF EXISTS settings") + + # 올바른 스키마로 생성 + cursor.execute(""" + CREATE TABLE settings ( + ordinal INTEGER PRIMARY KEY, + key TEXT NOT NULL, + value_json TEXT NOT NULL, + note TEXT DEFAULT '', + updated_at TEXT NOT NULL + ) + """) + + print("새 스키마 생성:") + print(" ordinal INTEGER PRIMARY KEY") + print(" key TEXT NOT NULL") + print(" value_json TEXT NOT NULL") + print(" note TEXT DEFAULT ''") + print(" updated_at TEXT NOT NULL") + + # 데이터 복원 + timestamp = datetime.now().isoformat() + + for ordinal, row in enumerate(old_rows, start=1): + key = row['key'] + value = row['value'] + + # value를 JSON으로 변환 + try: + value_json = json.dumps(str(value), ensure_ascii=False) + except: + value_json = json.dumps("", ensure_ascii=False) + + cursor.execute(""" + INSERT INTO settings (ordinal, key, value_json, note, updated_at) + VALUES (?, ?, ?, ?, ?) + """, (ordinal, key, value_json, "", timestamp)) + + conn.commit() + + # 검증 + cursor.execute("SELECT COUNT(*) FROM settings") + count = cursor.fetchone()[0] + print(f"\n복원된 settings: {count}개 행") + + cursor.execute("SELECT ordinal, key, value_json FROM settings LIMIT 3") + print("샘플 데이터:") + for ordinal, key, value_json in cursor.fetchall(): + value = json.loads(value_json) + print(f" {ordinal}. {key} = {value}") + + conn.close() + + print(f"\n[OK] settings 테이블 스키마 수정 완료") + +if __name__ == "__main__": + fix_settings_schema_v2() diff --git a/tools/initialize_snapshot_admin_db.py b/tools/initialize_snapshot_admin_db.py new file mode 100644 index 0000000..8923e1a --- /dev/null +++ b/tools/initialize_snapshot_admin_db.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +snapshot_admin.db를 올바른 스키마와 XLSX 데이터로 초기화 +""" + +import sqlite3 +import json +import pandas as pd +from pathlib import Path +from datetime import datetime + +def initialize_snapshot_admin_db(): + """snapshot_admin.db 초기화""" + + db_path = Path('src/quant_engine/snapshot_admin.db') + xlsx_file = Path('GatherTradingData.xlsx') + json_file = Path('GatherTradingData.json') + + print("="*80) + print("snapshot_admin.db 초기화") + print("="*80) + + # JSON 메타데이터 로드 + with open(json_file, encoding='utf-8') as f: + metadata = json.load(f).get('metadata', {}) + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 1. settings 테이블 초기화 + print("\n[1] settings 테이블 초기화") + cursor.execute("DROP TABLE IF EXISTS settings") + cursor.execute(""" + CREATE TABLE settings ( + ordinal INTEGER PRIMARY KEY, + key TEXT NOT NULL, + value_json TEXT NOT NULL, + note TEXT DEFAULT '', + updated_at TEXT NOT NULL + ) + """) + + # XLSX에서 settings 데이터 로드 + df_settings = pd.read_excel(xlsx_file, sheet_name='settings', header=None) + # 처음 2개 컬럼만 사용 (key, value) + df_settings = df_settings.iloc[:, :2] + df_settings.columns = ['key', 'value'] + timestamp = datetime.now().isoformat() + + print(f" {len(df_settings)}개 설정 로드 중...") + for ordinal, (idx, row) in enumerate(df_settings.iterrows(), start=1): + key = str(row['key']) + value = str(row['value']) + value_json = json.dumps(value, ensure_ascii=False) + + cursor.execute(""" + INSERT INTO settings (ordinal, key, value_json, note, updated_at) + VALUES (?, ?, ?, ?, ?) + """, (ordinal, key, value_json, "", timestamp)) + + cursor.execute("SELECT COUNT(*) FROM settings") + count = cursor.fetchone()[0] + print(f" [OK] {count}개 설정 로드") + + # 2. account_snapshot 테이블 초기화 + print("\n[2] account_snapshot 테이블 초기화") + cursor.execute("DROP TABLE IF EXISTS account_snapshot") + cursor.execute(""" + CREATE TABLE account_snapshot ( + ordinal INTEGER PRIMARY KEY, + row_json TEXT NOT NULL, + captured_at TEXT NOT NULL DEFAULT '', + account TEXT NOT NULL DEFAULT '', + account_type TEXT NOT NULL DEFAULT '', + ticker TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL DEFAULT '', + parse_status TEXT NOT NULL DEFAULT '', + user_confirmed TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL + ) + """) + + # XLSX에서 account_snapshot 데이터 로드 + df_snapshot = pd.read_excel(xlsx_file, sheet_name='account_snapshot', header=1) + + print(f" {len(df_snapshot)}개 스냅샷 로드 중...") + for ordinal, (idx, row) in enumerate(df_snapshot.iterrows(), start=1): + row_dict = row.to_dict() + + # row_json으로 저장 + row_json = json.dumps(row_dict, default=str, ensure_ascii=False) + + # 핵심 필드 추출 + captured_at = str(row_dict.get('captured_at', '')) + account = str(row_dict.get('account', '')) + account_type = str(row_dict.get('account_type', '')) + ticker = str(row_dict.get('ticker', '')) + name = str(row_dict.get('name', '')) + parse_status = str(row_dict.get('parse_status', '')) + user_confirmed = str(row_dict.get('user_confirmed', '')) + + cursor.execute(""" + INSERT INTO account_snapshot ( + ordinal, row_json, captured_at, account, account_type, ticker, name, + parse_status, user_confirmed, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + ordinal, row_json, captured_at, account, account_type, ticker, name, + parse_status, user_confirmed, timestamp + )) + + cursor.execute("SELECT COUNT(*) FROM account_snapshot") + count = cursor.fetchone()[0] + print(f" [OK] {count}개 스냅샷 로드") + + # 3. 인덱스 생성 + print("\n[3] 인덱스 생성") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_account_snapshot_captured_at ON account_snapshot(captured_at)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_account_snapshot_ticker ON account_snapshot(ticker)") + print(f" [OK] 인덱스 생성") + + conn.commit() + conn.close() + + # 검증 + print("\n[검증]") + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + cursor.execute("SELECT COUNT(*) FROM settings") + settings_count = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM account_snapshot") + snapshot_count = cursor.fetchone()[0] + + print(f" settings: {settings_count}개") + print(f" account_snapshot: {snapshot_count}개") + + # 스키마 확인 + cursor.execute("PRAGMA table_info(settings)") + settings_cols = [col[1] for col in cursor.fetchall()] + print(f" settings 컬럼: {settings_cols}") + + cursor.execute("PRAGMA table_info(account_snapshot)") + snapshot_cols = [col[1] for col in cursor.fetchall()] + print(f" account_snapshot 컬럼: {snapshot_cols[:5]}... ({len(snapshot_cols)} 개)") + + conn.close() + + print("\n[OK] snapshot_admin.db 초기화 완료") + +if __name__ == "__main__": + initialize_snapshot_admin_db() diff --git a/tools/load_from_xlsx_correct.py b/tools/load_from_xlsx_correct.py index 7f1d87c..b162a05 100644 --- a/tools/load_from_xlsx_correct.py +++ b/tools/load_from_xlsx_correct.py @@ -32,7 +32,7 @@ class CorrectXLSXLoader: return data.get('metadata', {}) def load_excel_sheets(self, metadata: dict) -> dict: - """Excel에서 올바른 header를 사용해서 모든 시트 로드""" + """Excel에서 올바른 header를 사용해서 모든 시트 로드 (account_snapshot 제외)""" print("[로드 중] Excel 파일 읽기...") sheet_headers = metadata.get('sheet_headers', {}) @@ -43,6 +43,11 @@ class CorrectXLSXLoader: sheets_data = {} for sheet_name in sheet_names: + # account_snapshot은 건너뛴다 (별도 처리) + if sheet_name == 'account_snapshot': + print(f" [SKIP] {sheet_name} (수동 처리)") + continue + # metadata에서 header_row_1based 읽기 header_info = sheet_headers.get(sheet_name, {}) header_row_1based = header_info.get('header_row_1based', 1) @@ -86,7 +91,13 @@ class CorrectXLSXLoader: try: conn = sqlite3.connect(db_path) - df.to_sql(sheet_name, conn, if_exists='replace', index=False) + + # account_snapshot은 특별하게 처리: 스키마를 보존하면서 데이터만 추가 + if sheet_name == 'account_snapshot': + self._load_account_snapshot(conn, df) + else: + df.to_sql(sheet_name, conn, if_exists='replace', index=False) + conn.close() print(f" [OK] {sheet_name}: {len(df)} rows → {db_path.name}") @@ -100,6 +111,45 @@ class CorrectXLSXLoader: print(f" [FAIL] {sheet_name}: {str(e)[:80]}") self.results["errors"].append(sheet_name) + def _load_account_snapshot(self, conn: sqlite3.Connection, df: pd.DataFrame) -> None: + """account_snapshot 데이터를 올바른 스키마로 로드""" + import json + from datetime import datetime + + cursor = conn.cursor() + timestamp = datetime.now().isoformat() + + # 기존 데이터 삭제 (옵션: DELETE 또는 유지) + cursor.execute("DELETE FROM account_snapshot") + + for ordinal, row in enumerate(df.iterrows(), start=1): + idx, series = row + row_dict = series.to_dict() + + # row_json으로 저장 + row_json = json.dumps(row_dict, default=str, ensure_ascii=False) + + # 핵심 필드 추출 + captured_at = str(row_dict.get('captured_at', '')) + account = str(row_dict.get('account', '')) + account_type = str(row_dict.get('account_type', '')) + ticker = str(row_dict.get('ticker', '')) + name = str(row_dict.get('name', '')) + parse_status = str(row_dict.get('parse_status', '')) + user_confirmed = str(row_dict.get('user_confirmed', '')) + + cursor.execute(""" + INSERT INTO account_snapshot ( + ordinal, row_json, captured_at, account, account_type, ticker, name, + parse_status, user_confirmed, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + ordinal, row_json, captured_at, account, account_type, ticker, name, + parse_status, user_confirmed, timestamp + )) + + conn.commit() + def verify(self) -> None: """로드 검증""" print("\n[검증 중...]") diff --git a/tools/test_api_components.py b/tools/test_api_components.py new file mode 100644 index 0000000..7281c2e --- /dev/null +++ b/tools/test_api_components.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +API 핸들러의 각 컴포넌트 테스트 +""" + +import sys +sys.path.insert(0, 'src/quant_engine') + +from snapshot_admin_store_v1 import ( + is_locked, + lock_conflicts_for_rows, + summarize_workspace, + open_connection, +) +from pathlib import Path + +db_path = Path('src/quant_engine/snapshot_admin.db') + +print("="*80) +print("API 컴포넌트 테스트") +print("="*80) + +# 1. is_locked 테스트 +print("\n[1] is_locked 테스트") +try: + locked = is_locked(db_path, "settings") + print(f" [OK] is_locked result: {locked}") +except Exception as e: + print(f" [ERROR] {e}") + +# 2. lock_conflicts_for_rows 테스트 +print("\n[2] lock_conflicts_for_rows 테스트") +try: + test_rows = [ + { + "ordinal": 5, + "key": "total_asset_krw", + "value": "450000000", + "note": "test" + } + ] + + conflicts = lock_conflicts_for_rows(db_path, "settings", test_rows) + print(f" [OK] conflicts: {conflicts}") +except Exception as e: + print(f" [ERROR] {e}") + +# 3. summarize_workspace 테스트 +print("\n[3] summarize_workspace 테스트") +try: + import time + start = time.time() + summary = summarize_workspace(db_path) + elapsed = time.time() - start + print(f" [OK] summarize_workspace completed in {elapsed:.2f}s") + print(f" Keys: {list(summary.keys())[:5]}") +except Exception as e: + print(f" [ERROR] {e}") + import traceback + traceback.print_exc() + +print("\n[완료]") diff --git a/tools/test_api_settings_save.py b/tools/test_api_settings_save.py new file mode 100644 index 0000000..838a3f9 --- /dev/null +++ b/tools/test_api_settings_save.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" +/api/settings/save 엔드포인트 테스트 +""" + +import requests +import json +import time + +BASE_URL = "http://localhost:5000" + +print("="*80) +print("/api/settings/save 엔드포인트 테스트") +print("="*80) + +# 데이터 준비 +test_data = { + "rows": [ + { + "ordinal": 5, + "key": "total_asset_krw", + "value": "500000000", + "note": "API 테스트 수정" + } + ] +} + +print(f"\n[요청] POST {BASE_URL}/api/settings/save") +print(f"[데이터] {json.dumps(test_data, ensure_ascii=False, indent=2)}") + +try: + start = time.time() + response = requests.post( + f"{BASE_URL}/api/settings/save", + json=test_data, + timeout=10 + ) + elapsed = time.time() - start + + print(f"\n[응답 시간] {elapsed:.2f}s") + print(f"[상태 코드] {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f"[결과] {json.dumps(result, ensure_ascii=False, indent=2)}") + print(f"\n[OK] /api/settings/save 성공") + else: + print(f"[오류 응답]") + print(f" 상태: {response.status_code}") + print(f" 본문: {response.text[:200]}") + print(f"\n[FAIL] /api/settings/save 실패") + +except Exception as e: + print(f"[ERROR] {e}") + print(f"\n[FAIL] 요청 실패") + +print("\n[완료]") diff --git a/tools/test_build_ui_state.py b/tools/test_build_ui_state.py new file mode 100644 index 0000000..1fc966e --- /dev/null +++ b/tools/test_build_ui_state.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +build_ui_state 함수의 각 단계 테스트 +""" + +import sys +sys.path.insert(0, '.') + +# Import from package +import importlib.util +spec = importlib.util.spec_from_file_location( + "snapshot_admin_store_v1", + "src/quant_engine/snapshot_admin_store_v1.py" +) +store = importlib.util.module_from_spec(spec) +spec.loader.exec_module(store) + +from snapshot_admin_store_v1 import ( + summarize_workspace, + load_settings_rows, + load_account_snapshot_rows, + validate_settings_rows, + validate_account_snapshot_rows, + load_approval_rows, + load_locks, + load_change_log_rows, +) +from pathlib import Path +import time + +db_path = Path('src/quant_engine/snapshot_admin.db') + +print("="*80) +print("build_ui_state 함수 단계별 테스트") +print("="*80) + +# 1. summarize_workspace +print("\n[1] summarize_workspace") +try: + start = time.time() + result = summarize_workspace(db_path) + elapsed = time.time() - start + print(f" [OK] {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 2. load_settings_rows +print("\n[2] load_settings_rows") +try: + start = time.time() + result = load_settings_rows(db_path) + elapsed = time.time() - start + print(f" [OK] {len(result)} rows, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 3. load_account_snapshot_rows +print("\n[3] load_account_snapshot_rows") +try: + start = time.time() + result = load_account_snapshot_rows(db_path) + elapsed = time.time() - start + print(f" [OK] {len(result)} rows, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 4. validate_settings_rows +print("\n[4] validate_settings_rows") +try: + start = time.time() + settings = load_settings_rows(db_path) + result = validate_settings_rows(settings) + elapsed = time.time() - start + print(f" [OK] {len(result)} errors, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 5. validate_account_snapshot_rows +print("\n[5] validate_account_snapshot_rows") +try: + start = time.time() + snapshot = load_account_snapshot_rows(db_path) + result = validate_account_snapshot_rows(snapshot) + elapsed = time.time() - start + print(f" [OK] {len(result)} errors, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 6. load_approval_rows +print("\n[6] load_approval_rows") +try: + start = time.time() + result = load_approval_rows(db_path) + elapsed = time.time() - start + print(f" [OK] {len(result)} rows, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 7. load_locks +print("\n[7] load_locks") +try: + start = time.time() + result = load_locks(db_path) + elapsed = time.time() - start + print(f" [OK] {len(result)} rows, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 8. load_change_log_rows +print("\n[8] load_change_log_rows") +try: + start = time.time() + result = load_change_log_rows(db_path, limit=12) + elapsed = time.time() - start + print(f" [OK] {len(result)} rows, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + +# 9. Full build_ui_state (at the end) +print("\n[9] build_ui_state (FULL)") +try: + start = time.time() + result = build_ui_state(db_path) + elapsed = time.time() - start + print(f" [OK] keys={len(result)}, {elapsed:.2f}s") +except Exception as e: + print(f" [ERROR] {e}") + import traceback + traceback.print_exc() + +print("\n[완료]") diff --git a/tools/test_build_ui_state_simple.py b/tools/test_build_ui_state_simple.py new file mode 100644 index 0000000..8004eaa --- /dev/null +++ b/tools/test_build_ui_state_simple.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +build_ui_state 함수의 각 단계 테스트 - 직접 호출 +""" + +import sys +sys.path.insert(0, '.') + +# Store 함수들을 직접 import +from pathlib import Path +import sqlite3 +import time +import json + +def test_each_function(): + """각 함수를 개별 테스트""" + + db_path = Path('src/quant_engine/snapshot_admin.db') + + print("="*80) + print("Database 함수 단계별 테스트") + print("="*80) + + # 1. 테이블 존재 확인 + print("\n[테이블 확인]") + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") + tables = [row[0] for row in cursor.fetchall()] + print(f" 테이블 수: {len(tables)}") + print(f" 주요 테이블: settings, account_snapshot, workspace_approval_v2") + + # 각 테이블의 행 수 + for table in ['settings', 'account_snapshot', 'workspace_approval_v2', 'workspace_change_log']: + cursor.execute(f"SELECT COUNT(*) FROM {table}") + count = cursor.fetchone()[0] + print(f" {table}: {count} rows") + + # 2. settings SELECT 테스트 + print("\n[settings SELECT 테스트]") + try: + cursor.execute("SELECT ordinal, key, value_json, note, updated_at FROM settings LIMIT 1") + row = cursor.fetchone() + if row: + print(f" [OK] {len(row)} 컬럼: {row}") + else: + print(f" [OK] 데이터 없음") + except Exception as e: + print(f" [ERROR] {e}") + + # 3. account_snapshot SELECT 테스트 + print("\n[account_snapshot SELECT 테스트]") + try: + cursor.execute("SELECT ordinal, row_json, captured_at, account, account_type, ticker, name, parse_status, user_confirmed, updated_at FROM account_snapshot LIMIT 1") + row = cursor.fetchone() + if row: + print(f" [OK] {len(row)} 컬럼") + else: + print(f" [OK] 데이터 없음") + except Exception as e: + print(f" [ERROR] {e}") + + # 4. workspace_approval_v2 SELECT 테스트 + print("\n[workspace_approval_v2 SELECT 테스트]") + try: + cursor.execute("SELECT domain, target_ref, status, approved_by, approved_at, note, updated_at FROM workspace_approval_v2 LIMIT 1") + row = cursor.fetchone() + if row: + print(f" [OK] {len(row)} 컬럼") + else: + print(f" [OK] 데이터 없음") + except Exception as e: + print(f" [ERROR] {e}") + + # 5. workspace_change_log SELECT 테스트 + print("\n[workspace_change_log SELECT 테스트]") + try: + cursor.execute("SELECT id, domain, action, target_ref, actor, note, before_json, after_json, created_at FROM workspace_change_log LIMIT 1") + row = cursor.fetchone() + if row: + print(f" [OK] {len(row)} 컬럼") + else: + print(f" [OK] 데이터 없음") + except Exception as e: + print(f" [ERROR] {e}") + + conn.close() + + print("\n[완료]") + +if __name__ == "__main__": + test_each_function() diff --git a/tools/test_import.py b/tools/test_import.py new file mode 100644 index 0000000..46bba05 --- /dev/null +++ b/tools/test_import.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 + +import sys +sys.path.insert(0, '.') + +try: + from src.quant_engine import snapshot_admin_server_v1 + print("[OK] 임포트 성공") +except Exception as e: + print(f"[ERROR] 임포트 실패: {e}") + import traceback + traceback.print_exc() diff --git a/tools/test_remaining_apis.py b/tools/test_remaining_apis.py new file mode 100644 index 0000000..af8edba --- /dev/null +++ b/tools/test_remaining_apis.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +/api/state, /api/export 등 다른 엔드포인트 테스트 +""" + +import requests +import json +import time + +BASE_URL = "http://localhost:5000" + +print("="*80) +print("API 엔드포인트 테스트") +print("="*80) + +# 1. /api/state 테스트 +print("\n[1] GET /api/state") +try: + start = time.time() + response = requests.get(f"{BASE_URL}/api/state", timeout=15) + elapsed = time.time() - start + + print(f" 응답 시간: {elapsed:.2f}s") + print(f" 상태 코드: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f" [OK] 키: {list(result.keys())[:5]}") + else: + print(f" [FAIL] {response.text[:100]}") + +except Exception as e: + print(f" [ERROR] {e}") + +# 2. /api/export 테스트 +print("\n[2] GET /api/export") +try: + start = time.time() + response = requests.get(f"{BASE_URL}/api/export", timeout=15) + elapsed = time.time() - start + + print(f" 응답 시간: {elapsed:.2f}s") + print(f" 상태 코드: {response.status_code}") + print(f" 응답 크기: {len(response.text)} bytes") + + if response.status_code == 200: + try: + result = response.json() + print(f" [OK] 키: {list(result.keys())[:3]}") + except: + print(f" [OK] (JSON 파싱 불가, 바이너리일 수 있음)") + else: + print(f" [FAIL] {response.text[:100]}") + +except Exception as e: + print(f" [ERROR] {e}") + +# 3. /api/tables 테스트 +print("\n[3] GET /api/tables") +try: + start = time.time() + response = requests.get(f"{BASE_URL}/api/tables", timeout=15) + elapsed = time.time() - start + + print(f" 응답 시간: {elapsed:.2f}s") + print(f" 상태 코드: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f" [OK] {len(result)} 테이블") + for table in result[:3]: + print(f" - {table['table']}: {table['row_count']} rows") + else: + print(f" [FAIL] {response.text[:100]}") + +except Exception as e: + print(f" [ERROR] {e}") + +print("\n[완료]") diff --git a/tools/verify_data_load.py b/tools/verify_data_load.py new file mode 100644 index 0000000..f79bc50 --- /dev/null +++ b/tools/verify_data_load.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +데이터베이스 로드 상태 검증 +""" + +import sqlite3 +from pathlib import Path + +def verify_databases(): + """두 데이터베이스의 상태 확인""" + + kis_db = Path('src/quant_engine/kis_data_collection.db') + snapshot_db = Path('src/quant_engine/snapshot_admin.db') + + print("="*80) + print("데이터베이스 로드 상태 검증") + print("="*80) + + for db_name, db_path in [("kis_data_collection", kis_db), ("snapshot_admin", snapshot_db)]: + print(f"\n[{db_name}]") + + if not db_path.exists(): + print(f" 파일 없음") + continue + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 테이블 목록 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") + tables = [row[0] for row in cursor.fetchall()] + print(f" 테이블 수: {len(tables)}") + print(f" 테이블: {', '.join(tables[:5])}..." if len(tables) > 5 else f" 테이블: {', '.join(tables)}") + + # 각 테이블의 행 수 + print(f"\n 테이블별 행 수:") + total_rows = 0 + for table in sorted(tables): + try: + cursor.execute(f"SELECT COUNT(*) FROM {table}") + count = cursor.fetchone()[0] + if count > 0: + print(f" {table}: {count:,}") + total_rows += count + except: + pass + + print(f" 총 행 수: {total_rows:,}") + + conn.close() + + print("\n[완료]") + +if __name__ == "__main__": + verify_databases()