2. 코드 전수 분석
TL;DR
KSEL 7-KPI 평가용 풀스택 모놀리식 PoC. FastAPI 백엔드 + React 19 SPA + nginx 정적 + Claude Agent SDK 통합. 단일 EC2 (t4g.xlarge AL2023) · alpha push → GitHub Actions → rsync → Infisical export → rolling restart → smoke (deploy_id 일치). 3-tier requirements (vercel stub / EC2 real / dev) 로 250MB Vercel 제약과 ~7GB HF 모델을 같은 코드베이스에서 처리.
2.1 레포 구조
| 경로 | 파일 | 용량 | 역할 |
|---|---|---|---|
src/ | 48 | 404 KB | 도메인 · KPI · 어댑터 · 메트릭 · Agent |
api/ | 9 | 92 KB | FastAPI 진입점 · chat · agent · ksel |
frontend/ | 55 | 524 KB | React 19 SPA · 4 페이지 |
tests/ | 31 | 172 KB | pytest (alpha 머지 전 기준) |
datasets/ | 23 | 10 MB | KPI 평가 (jsonl + csv) |
deploy/ | 5 | 20 KB | Caddyfile · setup-ec2.sh |
scripts/ | 14 | 96 KB | build_datasets · deploy.sh |
evidence/ | 5 | 252 KB | KSEL 시험의뢰서 · 기술 PDF |
2.2 백엔드 핵심
진입점
- FastAPI app
src/api.py:235— title "TIPS R&D KPI Evaluation API" v0.6.0- Vercel 진입
api/index.py— builder 가app심볼 인식- 로컬
uvicorn src.api:app --host 0.0.0.0 --port 8000- 전역 시드
SEED=20260514— KPI별 stub 결정성- 미들웨어
x-request-id주입 · CORS · 구조화 로깅 ContextVar
7 KPI 구현 매트릭스
| KPI | 모듈 | 데이터셋 | 어댑터 | 합격선 |
|---|---|---|---|---|
| ① 재무 F1 | kpi/kpi1_finance_qa.py | 1,050건 | pipelines/agents:classify_scenario (16 시나리오) | 71.07 |
| ② 분류 | kpi/kpi2_text_classification.py | 1,000건 | pipelines/thread_titles:classify_intent | 99 % |
| ③ BLEU | kpi/kpi3_translation.py | 500건 | pipelines/finetune_inference:generate (vLLM + Qwen-ORPO / stub) | 78 |
| ④ 처리속도 | kpi/kpi4_throughput.py | 10,000건 | pipelines/data_augmentation:augment_record | 500/min + 99.9 % |
| ⑤ 개인화 | kpi/kpi5_personalized_recommendation.py | 500건 | pipelines/recommendations:score_personalization | 0.31 |
| ⑥ NQ 검색 | kpi/kpi6_finance_search.py | 1,000건 | pipelines/search:search_top_k (Pinecone/Faiss / BM25) | 64.06 |
| ⑦ 상품추천 | kpi/kpi7_product_recommendation.py | 1,000건 | pipelines/recommendations:recommend_top_k | 86 |
AI 모델 어댑터 (src/ai_clients/)
| 클라이언트 | generate | embed |
|---|---|---|
| AnthropicClient | O (Claude) | X (raise) |
| OpenAIClient | O | O |
| EmbeddingClient | X | O (sentence-transformers) |
| LocalModelClient | O (vLLM) | — |
| GeminiClient | O | — |
Claude Agent (/api/chat, /api/agent/*)
- 구현
src/agents/financial_agent.py::FinancialAgent- 모델
- 기본
claude-opus-4-7· max_tokens 4096 · effort "high" · max_turns 6 (운영은 sonnet-4-5) - Prompt cache
- 시스템 + 8 도구에
cache_control={"type":"ephemeral"}— ~10× 절감 - Fallback
- Claude → OpenAI gpt-4o-mini → canned 응답
등록 8 도구 (src/agents/tools.py)
get_portfolio_overviewget_recent_transactionsclassify_intent(KPI ②)recommend_products(KPI ⑦)simulate_financial_plansearch_finance_knowledge(KPI ⑥)calculate_dti_ltvclassify_finance_scenario(KPI ①)
측정 결과 & 로깅
- 결과 파일:
RESULTS_DIR=/app/results·{kpi_id}_{UTC}.json - NDJSON:
KPI_NDJSON_DUMP=1시/tmp/results/runs/{run_id}.ndjson - 로깅:
JsonFormatter한 줄 JSON · ContextVar (request_id/run_id/kpi) - 이벤트:
api.boot·api.request.done·kpi.bulk.{start,progress,done}·kpi.single.measured·result.saved
페르소나 6명 (api/personas_data.py)
| ID | 나이 | 직업 | 자산 | 위험도 |
|---|---|---|---|---|
| jiyu | 25 | 스타트업 마케터 | 1,200만 | high |
| minjae | 35 | IT 부장 | 8,500만 | mid |
| eunyoung | 62 | 은퇴 | 4억 | low |
| soyeon | 28 | 프리랜서 디자이너 | 1,800만 | high |
| junho | 38 | 개업 의사 | 1.5억 | mid |
| jaesung | 55 | 공무원 | 2.5억 | low |
2.3 프론트엔드 (React 19 SPA)
스택
- 프레임워크
- React 19.0.0 · Vite 6.0.0 · TS 5.6.0 · pnpm 10.20
- 라우팅
- react-router-dom 7.1
- 상태
- React Context × 3 + TanStack Query 5.59
- 스타일
- Tailwind CSS 4.0 (Vite plugin)
- 차트
- recharts 3.8.1
- CSV
- papaparse 5.5.3
4 페이지 + KPI 매핑
| 경로 | 역할 | KPI |
|---|---|---|
/ | 대시보드 — 페르소나별 자산·거래·차트 | — |
/chat | AI 어시스턴트 + 측정 패널 + CSV 배치 | ①②③⑤⑥ |
/recommend | Top-10 추천 + 적합도 ring | ⑦ |
/analysis | 10/15/20년 재무 시뮬 + 처리속도 1분×5회 | ④ |
/admin/logs | 실시간 로그 뷰어 SSE · CSV/JSONL 다운로드 | 전체 |
측정 컨텍스트 (3 Context)
- PersonaContext — localStorage + StorageEvent 멀티탭 동기화
- KpiHistoryContext — 대화별 KPI ①②③⑤⑥ 누적 (~500건)
- Kpi4MeasurementContext — 1분×5회 정밀 검증 백그라운드 보존
2.4 인프라 · CI/CD
워크플로
| 워크플로 | 트리거 | 역할 |
|---|---|---|
ci-cd-alpha.yml | alpha push | EC2 자동 배포 (concurrency 직렬화) |
ci.yml | PR/push | 경로 필터 분기 — api/frontend/docker-build |
배포 흐름 (scripts/deploy.sh)
- SSH 키 로드 (
webfactory/ssh-agent) - rsync 동기화 (
.git/node_modules/.env*제외) - 원격
infisical export --env=beta --path=/fingu-tips --format=dotenv→.env DEPLOY_ID=${{github.sha}}appenddocker compose build --quiet→ rolling restart (api healthcheck wait → web)- 외부 smoke —
/api/health.deploy_id == github.sha매칭 (5회 지수백오프)
Requirements 3-tier
| 파일 | 용도 | 크기 |
|---|---|---|
requirements.txt | Vercel stub | ~ 250 MB |
requirements-real.txt | EC2 real (HF 모델) | > 7 GB |
requirements-test.txt | CI unit | compact |
requirements-dev.txt | 로컬 개발 | full |
nginx (nginx.conf)
- HTTP/2 활성 · TLS 1.2/1.3 · Cloudflare Origin cert (
/etc/nginx/certs/origin.{crt,key}) - access log masked:
$request대신$request_method $uri $server_protocol /api/→http://fingu_api:8001· keepalive 32 · timeout 90s- 정적 자산
Cache-Control: max-age=604800, immutable - SPA fallback
try_files $uri $uri/ /index.html
2.5 테스트 · 증빙 · 데이터셋 (PR #62 이전)
- pytest 파일
- 31 개 · 3,289 LOC
- conftest
- autouse fixture
_clear_ai_secrets— 모든 AI 키 제거 - contract test
test_kpi_contract.py— 7 KPI parametrize · MIN_SAMPLES KSEL 기준 일치- 메트릭 단위
test_f1_score·test_bleu·test_nq_em·test_llm_rec_score·test_throughput- CI
pytest tests/ -x --tb=short -q --timeout=30 \|\| true— 실패해도 통과 (PR #62에서 보강)
데이터셋 (15,050 sample)
| KPI | 분량 | 출처 | 라이선스 | 라벨 |
|---|---|---|---|---|
| 1 | 1,050 | GPT-4o-mini 합성 | OpenAI 약관 | O |
| 2 | 1,000 | GPT-4o-mini | OpenAI 약관 | O |
| 3 | 500 | 참고문서 기재 | 참고문서 기재 | O |
| 4 | 10,000 | Faker seed 20260514 | MIT | X · 의도 |
| 5 | 500 | 참고문서 기재 | 참고문서 기재 | 참고용 |
| 6 | 1,000 | GPT-4o-mini | OpenAI 약관 | O |
| 7 | 1,000 + 카탈로그 | Faker | MIT | O |
evidence/
Technical-overview-of-data-collection-and-model-fine-tuning.pdf211 KBKSEL_시험의뢰서_2026-05-08.docx21 KB (제출 완료)TIPS_성능평가_의뢰서_참고자료_2026-05-08.docx15 KB
2.6 API 라우트 전체
| 메서드 | 경로 | 핸들러 (path:line) |
|---|---|---|
| GET | /api/health | src/api.py:358 |
| GET | /api/kpi | :386 |
| GET | /api/kpi/{n}/dataset | :417 |
| POST | /api/kpi/{n}/evaluate | :442 |
| POST | /api/kpi/4/evaluate?run=N | :504 |
| POST | /api/kpi/measure-all | :579 |
| GET | /api/kpi/{n}/results | :768 |
| GET | /api/kpi-summary | :784 |
| POST | /api/kpi/{n}/dataset/upload | :668 |
| POST | /api/kpi/{n}/evaluate/with-dataset | :717 |
| GET | /api/kpi/{n}/dataset/sample | :751 |
| POST | /api/kpi/{n}/sample | :978 |
| POST | /api/kpi/4/evaluate/stream | :1022 (SSE) |
| POST | /api/kpi/{n}/evaluate/stream | :1105 (SSE) |
| GET | /api/personas | :856 |
| GET | /api/personas/{id} | :862 |
| GET | /api/personas/{id}/portfolio | :874 |
| GET | /api/personas/{id}/recommendations | :919 |
| POST | /api/chat | :899 |
| POST | /api/analysis/simulate | :1260 |
| GET | /api/agent/health | api/agent.py:34 |
| GET | /api/agent/tools | api/agent.py:69 |
| POST | /api/agent/converse(/stream) | api/agent.py:113 |