Skip to content

Repository files navigation

Invest Jarvis

Next.js TypeScript Supabase Upstage vanilla-extract

DART 전자공시 원문을 벡터 DB에 저장하고, 사용자 질문과 의미적으로 유사한 공시를 실시간 검색해 Solar Pro2가 근거 기반으로 답변하는 RAG(Retrieval-Augmented Generation) 기반 AI 투자 비서


✨ 주요 기능

기능 설명
📡 실시간 공시 조회 DART Open API로 최신 공시 목록을 실시간 수집
🧩 금융 문맥 청킹 재무제표·감사의견 구조를 고려한 의미 단위 분할
🔍 벡터 유사도 검색 Solar Embedding 4096차원으로 의미 기반 공시 검색
💬 RAG 기반 답변 공시 원문을 프롬프트에 주입해 근거 있는 LLM 응답 생성
스트리밍 UI SSE → ReadableStream으로 첫 토큰부터 실시간 렌더링
🏢 비상장사 지원 종목코드 없이 기업명으로도 공시 임베딩·검색 가능

🏗️ 아키텍처

임베딩 파이프라인 (사전 실행)

POST /api/embed  {"stockCode": "005930"} or {"corpName": "비바리퍼블리카"}
      │
      ├─ DART corpCode.xml (ZIP, 115,550개 기업) → 24h 캐시
      ├─ DART list.json → 공시 목록
      ├─ DART document.xml → ZIP 해제 → EUC-KR/UTF-8 감지 → HTML 파싱
      ├─ FinancialTextSplitter → 600자 단위 금융 문맥 청킹
      ├─ solar-embedding-1-large-passage → 4096차원 벡터
      └─ Supabase disclosure_chunks 저장 (SSE로 진행 상황 실시간 전송)

RAG 채팅 파이프라인 (실시간)

POST /api/chat  {"messages": [...]}
      │
      ├─ 종목코드 감지 (6자리 정규식 + 기업명 사전 30개)
      ├─ solar-embedding-1-large-query → 질문 벡터화
      ├─ Supabase search_disclosures RPC → 유사 공시 청크 Top-5 반환
      ├─ 공시 원문을 Solar Pro2 system prompt에 주입
      └─ Solar Pro2 SSE → 서버에서 파싱 → ReadableStream → 클라이언트

프론트엔드 상태 흐름

ChatWindow (Client Component)
      └─ useChat() hook
              ├─ chatStatusAtom     idle / loading / streaming / error
              ├─ streamingContentAtom   실시간 누적 텍스트
              └─ chatMessagesAtom   완성된 대화 기록

🛠️ 기술적 결정

vanilla-extract — Zero-runtime CSS-in-TS

Tailwind는 클래스명 자동완성이 편리하지만 런타임 오버헤드가 있고, styled-components는 CSS-in-JS 방식의 JS 번들 비용이 있습니다. vanilla-extract는 빌드 타임에 정적 CSS로 컴파일되어 런타임 비용이 없고, TypeScript 타입 시스템으로 테마 토큰 오타를 빌드 단계에서 잡을 수 있습니다. 토스증권의 TDS와 동일한 기술 스택입니다.

Jotai — Atomic 상태 관리

스트리밍 응답은 초당 수십 번 상태가 변합니다. Zustand의 selector 기반 구독보다 Jotai의 Atomic 모델이 필요한 atom만 구독해 불필요한 리렌더링을 방지합니다. 파생 atom으로 isBusy, isStreaming 같은 계산된 상태를 선언적으로 정의할 수 있습니다.

Supabase pgvector — 별도 벡터 DB 없이 RAG 구현

Pinecone, Weaviate 같은 전용 벡터 DB 대신 PostgreSQL 확장인 pgvector를 선택했습니다. 기존 관계형 데이터와 벡터를 단일 DB에서 관리하고, Supabase 무료 티어로 인프라 비용 없이 시작할 수 있습니다.

Solar Embedding — 비대칭 임베딩으로 검색 정확도 향상

passage(문서 저장)와 query(검색 질의)를 별도 모델로 분리하는 비대칭 임베딩 구조가 RAG에 최적화되어 있습니다. 한국어·일본어·영어 MTEB 벤치마크에서 OpenAI text-embedding-3-large를 상회합니다.


⚡ 핵심 구현

1. DART corp_code 중복 문제 해결

DART의 corpCode.xml(115,550개 기업)에서 동일한 종목코드에 여러 corp_code가 매핑되는 케이스 발견. 자회사·관계사가 중복 등록된 경우로, 가장 낮은 corp_code(= 먼저 등록된 모법인)를 선택하도록 중복 skip 로직 추가.

// corp_code 오름차순 정렬 → 첫 번째 항목 = 모법인
if (result[stock_code] !== undefined) continue;

2. pgvector 4096차원 인덱스 제한 우회

Solar Embedding의 4096차원이 pgvector HNSW 인덱스의 최대 2000차원 제한을 초과. 인덱스 없이 Sequential Scan으로 운영. 현재 수천 건 규모에서는 성능 영향 없으며, 대규모 확장 시 PCA 차원 축소 또는 전용 벡터 DB 전환 예정.

3. SSE 스트리밍 파이프라인

Upstage의 SSE 응답(data: {"choices":[{"delta":{"content":"..."}}]})을 서버에서 파싱해 순수 텍스트만 ReadableStream으로 클라이언트 전달. 클라이언트는 TextDecoder로 누적하며 Jotai atom 업데이트 → React 리렌더링으로 실시간 타이핑 효과 구현.

4. EUC-KR 인코딩 자동 감지

오래된 DART 공시 문서는 EUC-KR 인코딩으로 저장됨. HTML <meta charset> 태그를 파싱해 인코딩을 자동 감지하고 TextDecoder에 적용.

function detectEncoding(data: Uint8Array): "euc-kr" | "utf-8" {
  const header = String.fromCharCode(...data.slice(0, 600));
  return /charset\s*=\s*["']?\s*euc-kr/i.test(header) ? "euc-kr" : "utf-8";
}

5. 비상장사 임베딩 지원

토스(비바리퍼블리카) 등 종목코드 없는 기업도 지원. 기업명 → 기업명 기반 인덱스(비상장 포함 전체) → corp_code 변환 후 임베딩.

# 종목코드 없이 기업명으로 임베딩
POST /api/embed  {"corpName": "비바리퍼블리카", "pageCount": 5}

📊 성과

응답 속도 (Chrome DevTools Network Timing 실측)

사용자 입력
    │
    ├── ~1.5초  TTFB (RAG 벡터 검색 + Solar 첫 토큰)
    │           ← 로딩 스켈레톤 표시 구간
    │
    ├── 첫 글자 → 실시간 타이핑 시작
    │
    └── 2~7초   스트리밍 완료 (답변 길이에 비례)
지표
TTFB ~1.5초
총 응답 완료 4~10초
RAG 벡터 검색 ~1.9초

RAG 검색 정확도 (Solar Embedding 실측)

Solar Embedding은 비대칭 구조 특성상 OpenAI 모델(0.7~0.9)보다 낮은 절대 유사도 범위를 가지며, 검색 성능은 상대적 랭킹 정확도로 평가합니다.

질문 유형 유사도
비관련 쿼리 (주가 질문) 0.36
일반 공시 쿼리 0.41 ~ 0.46
내용 직접 매칭 (재무제표 질의) 0.50+
  • 변별폭: 비관련 vs 직접 매칭 간 평균 0.14 차이 → 의미 있는 검색 변별력
  • 컨텍스트 히트율: 임베딩된 종목 질의 시 100%

🚀 시작하기

환경 설정

git clone https://github.com/seongkong/Invest_Jarvis.git
cd Invest_Jarvis
npm install
cp .env.local.example .env.local

.env.local에 아래 키를 입력합니다:

DART_API_KEY=             # https://opendart.fss.or.kr
UPSTAGE_API_KEY=          # https://console.upstage.ai
SUPABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
SUPABASE_ANON_KEY=

DB 초기화

Supabase SQL Editor에서 실행:

-- src/migrations/001_disclosure_chunks.sql

공시 임베딩

# 상장사 (종목코드)
curl -X POST http://localhost:3000/api/embed \
  -H "Content-Type: application/json" \
  -d '{"stockCode": "005930", "pageCount": 10}'

# 비상장사 (기업명)
curl -X POST http://localhost:3000/api/embed \
  -H "Content-Type: application/json" \
  -d '{"corpName": "비바리퍼블리카", "pageCount": 5}'

실행

npm run dev   # http://localhost:3000

📁 프로젝트 구조

src/
├── app/
│   ├── api/
│   │   ├── chat/route.ts          # RAG + Solar Pro2 스트리밍
│   │   ├── embed/route.ts         # 임베딩 파이프라인 (SSE)
│   │   └── dart/disclosures/      # 공시 목록 REST API
│   └── page.tsx
├── features/
│   ├── chat/                      # UI 컴포넌트, 훅, Jotai atoms
│   ├── dart/                      # DART API 클라이언트 & 쿼리
│   └── embedding/                 # 파이프라인, RAG 검색, Upstage 클라이언트
├── lib/supabase/                  # DB 클라이언트 & 타입
└── migrations/                    # pgvector 스키마 SQL

⚠️ 주의사항

  • .env.local은 절대 커밋하지 마세요 (.gitignore 포함됨)
  • SUPABASE_SERVICE_ROLE_KEY는 서버 전용입니다
  • Upstage Embedding API는 유료입니다. 임베딩 전 크레딧을 확인하세요
  • pgvector HNSW 인덱스는 4096차원 제한으로 미사용 (Sequential Scan 운영)

📄 라이선스

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages