fix: security, data-integrity, and doc-drift findings from repo audit

Consolidates duplicate KIS API client implementations (governance tests
were exercising an unused class instead of the one actually running in
production), closes a SQL injection path in the DB admin page, fixes a
migration that used MySQL-only syntax and had never actually applied
(confirmed against production), resyncs docs/db/quantengine.dbml with
all migrations, and removes a duplicate OMS·WMS·ERP frontend tree in
favor of src/frontend/. Also corrects several unverifiable/inflated
claims in the OMS planning docs and realigns CI/CD and architecture
documentation with what's actually in the repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 11:20:02 +09:00
parent 99943d9871
commit 70824c2afb
185 changed files with 2062 additions and 34521 deletions
+249 -2
View File
@@ -1,8 +1,14 @@
// =============================================================================
// QuantEngine Database Schema (DBML)
// DbUp 마이그레이션(V1~V5)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
// DbUp 마이그레이션(V1~V8, V003, V004)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
// (CLAUDE.md 규칙: schema 변경 → DBML + 문서 동기화)
//
// 마이그레이션 파일명 규칙 2종 혼재 (2026-07-30 발견, 미해결):
// V1__Name.sql .. V8__Name.sql (더블언더스코어, zero-pad 없음)
// V003_name.sql, V004_name.sql (싱글언더스코어, zero-pad)
// DbUp는 파일명 알파벳순으로 실행하므로 "V003" < "V1" 순서로 적용됨 — 신규 마이그레이션은
// 반드시 하나의 규칙(권장: V{n}__Name.sql)만 사용할 것.
//
// 참고: Hangfire 스키마는 Hangfire.PostgreSql 라이브러리가 자동 생성
// (DbUp 마이그레이션으로 관리하지 않음, 여기서도 제외)
// =============================================================================
@@ -382,7 +388,7 @@ Table engine_history.market_vs_engine_gap_history {
}
// =============================================================================
// Schema: engine_history (V5 normalized learning history)
// V6: Market Time Series (quantengine schema)
// =============================================================================
Table quantengine.price_history_daily {
@@ -415,6 +421,10 @@ Table quantengine.macro_history_daily {
}
}
// =============================================================================
// V5: Normalized Learning History (engine_history schema, event-sourcing style)
// =============================================================================
Table engine_history.source_observation {
observation_id UUID [pk]
observed_at TIMESTAMPTZ [not null]
@@ -493,6 +503,243 @@ Table engine_history.outcome_evaluation {
}
}
// =============================================================================
// V003: Audit Trail Tables (quantengine schema, 2026-07-24)
//
// 2026-07-30 확정 (실제 프로덕션 DB 조회로 검증): 이 마이그레이션은 CREATE TABLE 안에
// MySQL 전용 인라인 "INDEX name (cols)" 구문을 사용해 PostgreSQL에서 문법 오류로 실패했다.
// quantengine.schemaversions(DbUp 저널)에 V003이 아예 기록되어 있지 않고, 아래 3개 테이블도
// 프로덕션에 실제로 존재하지 않음을 직접 확인했다. V003_add_audit_trail_tables.sql의 인라인
// INDEX 구문은 이미 별도 CREATE INDEX 문으로 수정됐으므로, 다음 배포 시 DbUp가 이 마이그레이션을
// 최초로 실행해 아래 3개 테이블을 생성할 것이다.
// =============================================================================
Table quantengine.kis_collection_runs_audit {
id BIGSERIAL [pk]
run_id "UUID" [not null]
action "VARCHAR(10)" [not null, note: "INSERT/UPDATE/DELETE"]
changed_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
changed_by "VARCHAR(256)" [default: "CURRENT_USER"]
change_reason TEXT
old_values JSONB
new_values JSONB
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
Note: "kis_collection_runs 변경 이력 (트리거 자동 기록) — ⚠️ 마이그레이션 문법 오류로 실제 생성 여부 미확인"
}
Table quantengine.kis_collection_snapshots_audit {
id BIGSERIAL [pk]
snapshot_id "UUID" [not null]
action "VARCHAR(10)" [not null, note: "INSERT/UPDATE/DELETE"]
changed_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
changed_by "VARCHAR(256)" [default: "CURRENT_USER"]
change_reason TEXT
old_values JSONB
new_values JSONB
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
Note: "kis_collection_snapshots 변경 이력 — ⚠️ 마이그레이션 문법 오류로 실제 생성 여부 미확인"
}
Table quantengine.kis_collection_errors_audit {
id BIGSERIAL [pk]
error_id "UUID" [not null]
action "VARCHAR(10)" [not null, note: "INSERT/UPDATE/DELETE"]
changed_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
changed_by "VARCHAR(256)" [default: "CURRENT_USER"]
change_reason TEXT
old_values JSONB
new_values JSONB
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
Note: "kis_collection_errors 변경 이력 — ⚠️ 마이그레이션 문법 오류로 실제 생성 여부 미확인"
}
// =============================================================================
// V004: 3NF Normalization / Star Schema (quantengine schema, Adapter 패턴으로
// 기존 kis_collection_snapshots와 병행 운영 — 마이그레이션 자체 주석에 명시됨)
// =============================================================================
Table quantengine.stocks {
id SERIAL [pk]
ticker "VARCHAR(10)" [unique, not null]
name "VARCHAR(255)"
sector "VARCHAR(50)"
market "VARCHAR(20)" [note: "KOSPI/KOSDAQ 등"]
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
updated_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
Note: "종목 차원 테이블 (Star Schema dimension)"
}
Table quantengine.sources {
id SERIAL [pk]
name "VARCHAR(50)" [unique, not null]
priority INT [not null, note: "1=주 소스, 2 이상=폴백"]
fallback_to_id INT [ref: > quantengine.sources.id]
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
Note: "데이터 소스 차원 테이블 (KIS→Naver→Yahoo→OpenDART 폴백 체인)"
}
Table quantengine.market_data {
id BIGSERIAL [pk]
stock_id INT [not null, ref: > quantengine.stocks.id]
source_id INT [not null, ref: > quantengine.sources.id]
price DECIMAL [not null]
bid DECIMAL
ask DECIMAL
volume BIGINT
collected_at TIMESTAMPTZ [not null]
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
collection_run_id "UUID" [note: "kis_collection_runs 추적용"]
Note: "정규화된 시장 데이터 팩트 테이블 (Star Schema fact)"
}
Table quantengine.kis_collection_snapshots_v2 {
id "UUID" [pk]
run_id "UUID" [not null, ref: > quantengine.kis_collection_runs.run_id]
stock_id INT [not null, ref: > quantengine.stocks.id]
market_data_id BIGINT [ref: > quantengine.market_data.id, note: "조회 성능을 위한 의도적 역정규화"]
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
Note: "정규화된 kis_collection_snapshots — 레거시 kis_collection_snapshots와 Adapter 패턴으로 병행 운영, 완전 전환 여부 미확인"
}
// =============================================================================
// V8: PostgreSQL History-First Operating Model (quantengine schema)
//
// ⚠️ 스키마 충돌 주의: 아래 4개 테이블(market_raw_history, factor_version_history,
// factor_output_history, decision_result_history)은 engine_history 스키마(V3, 위 참고)에
// 이미 동일한 이름으로 존재한다. 컬럼 구조를 대조한 결과 같은 테이블의 재적용이 아니라
// 서로 다른 두 가지 설계다:
// - engine_history.*: EAV형 원본 관측 이력 (field_name/field_value 페어)
// - quantengine.*(이 섹션): OHLCV 와이드 테이블 / 팩터ID-스코어 구조
// 둘 다 실제 마이그레이션 파일에 존재하므로 DBML에는 두 스키마 버전을 모두 남긴다.
// 어느 쪽이 정본인지, 혹은 통합이 필요한지는 별도 아키텍처 결정 필요 (이번 작업 범위 밖).
// =============================================================================
Table quantengine.market_raw_history {
id BIGSERIAL [pk]
ticker "VARCHAR(32)" [not null]
as_of_date "VARCHAR(10)" [not null]
open_price "NUMERIC(18,4)"
high_price "NUMERIC(18,4)"
low_price "NUMERIC(18,4)"
close_price "NUMERIC(18,4)" [not null]
volume BIGINT
nav_price "NUMERIC(18,4)"
disparate_ratio "NUMERIC(10,6)"
tracking_error "NUMERIC(10,6)"
aum_krw "NUMERIC(20,2)"
raw_payload JSONB [not null]
provenance JSONB [not null]
created_at TIMESTAMPTZ [default: "NOW()"]
indexes {
(ticker, as_of_date) [unique, name: "uk_market_raw_ticker_date"]
}
Note: "OHLCV 와이드 테이블 — engine_history.market_raw_history(EAV형)와는 별개 설계"
}
Table quantengine.factor_version_history {
factor_id "VARCHAR(64)" [pk]
formula_name "VARCHAR(128)" [not null]
version "VARCHAR(32)" [not null]
category "VARCHAR(64)" [not null]
calibration_state "VARCHAR(32)" [not null, default: "'UNTESTED'"]
threshold_params JSONB [not null]
description TEXT
updated_at TIMESTAMPTZ [default: "NOW()"]
Note: "팩터 정의 — engine_history.factor_version_history와는 별개 설계 (PK가 factor_id 단독, 버전 이력 미보존)"
}
Table quantengine.factor_output_history {
id BIGSERIAL [pk]
run_id "VARCHAR(64)" [not null]
ticker "VARCHAR(32)" [not null]
as_of_date "VARCHAR(10)" [not null]
factor_id "VARCHAR(64)" [not null, ref: > quantengine.factor_version_history.factor_id]
score "NUMERIC(10,4)"
calculation_state "VARCHAR(32)" [not null]
provenance JSONB [not null]
created_at TIMESTAMPTZ [default: "NOW()"]
Note: "팩터 계산 결과 — engine_history.factor_output_history와는 별개 설계"
}
Table quantengine.decision_result_history {
id BIGSERIAL [pk]
run_id "VARCHAR(64)" [unique, not null]
as_of_date "VARCHAR(10)" [not null]
market_regime "VARCHAR(32)" [not null]
portfolio_health "VARCHAR(32)" [not null]
rebalance_required BOOLEAN [not null, default: "false"]
mid_check_required BOOLEAN [not null, default: "false"]
total_asset_krw "NUMERIC(20,2)" [not null]
d2_cash_krw "NUMERIC(20,2)" [not null]
decision_packet_json JSONB [not null]
created_at TIMESTAMPTZ [default: "NOW()"]
Note: "의사결정 패킷 이력 — engine_history.decision_result_history와는 별개 설계"
}
Table quantengine.order_waterfall_execution_history {
id BIGSERIAL [pk]
run_id "VARCHAR(64)" [not null, ref: > quantengine.decision_result_history.run_id]
ticker "VARCHAR(32)" [not null]
sell_priority_rank INT [not null]
waterfall_stage "VARCHAR(64)" [not null]
action "VARCHAR(16)" [not null]
target_qty INT [not null]
executed_qty INT [default: "0"]
target_price "NUMERIC(18,4)"
executed_price "NUMERIC(18,4)"
bid_ask_spread_bps "NUMERIC(10,2)"
slippage_bps "NUMERIC(10,2)"
status "VARCHAR(32)" [not null]
rationale TEXT
created_at TIMESTAMPTZ [default: "NOW()"]
Note: "매도 워터폴 실행 이력"
}
Table quantengine.shadow_ledger_history {
id BIGSERIAL [pk]
run_id "VARCHAR(64)" [not null, ref: > quantengine.decision_result_history.run_id]
ticker "VARCHAR(32)" [not null]
blocked_gate "VARCHAR(64)" [not null]
blocked_reason TEXT [not null]
shadow_price "NUMERIC(18,4)" [not null]
shadow_qty INT [not null]
shadow_tp_price "NUMERIC(18,4)"
shadow_sl_price "NUMERIC(18,4)"
created_at TIMESTAMPTZ [default: "NOW()"]
Note: "게이트에 막힌 주문의 가상 체결 감사 기록 (Shadow Ledger)"
}
Table quantengine.scheduler_state_history {
id BIGSERIAL [pk]
task_name "VARCHAR(64)" [not null]
execution_id "VARCHAR(64)" [unique, not null]
state "VARCHAR(32)" [not null]
started_at TIMESTAMPTZ [not null, default: "NOW()"]
finished_at TIMESTAMPTZ
error_message TEXT
lock_token "VARCHAR(64)"
indexes {
(task_name, state) [name: "idx_scheduler_state_task"]
}
Note: "스케줄러 작업 상태 머신 이력"
}
// =============================================================================
// Relationships (Logical, not enforced as FKs in DDL)
// =============================================================================