Files
connectai/src/features/stocks/stocksStore.ts
T
g1nation 0a97324f1b feat: v2.2.92 → v2.2.158 — god-file 분해 + Stocks feature + 대화 연속성
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules)
  · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등)
  · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks)
  · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등)

R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리)

Stocks feature 신규: /stocks slash command (v2.2.152~158)
  · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신
  · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR)
  · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴
  · LLM Top 5 매력도 분석 + Telegram 자동 보고서
  · KST 09:00/15:00 watcher 자동 모니터링

대화 연속성 (v2.2.150~157):
  · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor
  · thin follow-up 분류 → boilerplate 헤더 suppression
  · slash 명령 결과 chatHistory mirror (capture wrapper)
  · echo/parrot 금지 system prompt rule

기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:59:32 +09:00

97 lines
3.9 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { logError, logInfo } from '../../utils';
import type { Stock, StocksStore } from './types';
/**
* 워크스페이스 루트의 `.astra/stocks.json` 을 source of truth 로 사용.
*
* 결정 근거 (q1=A): 사용자가 워크스페이스 단위로 다른 종목 리스트를 가질 수 있게.
* 워크스페이스가 없으면 (= VS Code 가 폴더 열지 않고 시작) 빈 store 반환 — 이 경우
* watcher / slash 명령 모두 silent skip.
*
* Atomic write: tmp 파일에 쓰고 rename — 동시 read 또는 SIGKILL 중간에도 partial JSON
* 으로 안 깨지게.
*/
const STORE_REL_PATH = '.astra/stocks.json';
export function getStocksFilePath(): string | null {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) return null;
return path.join(folders[0].uri.fsPath, STORE_REL_PATH);
}
/** 파일 없으면 빈 배열 반환. 파일 파싱 실패해도 빈 배열 + 에러 로그. */
export function readStocksStore(): StocksStore {
const filePath = getStocksFilePath();
if (!filePath || !fs.existsSync(filePath)) return [];
try {
const raw = fs.readFileSync(filePath, 'utf-8');
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
logError('stocks.json 가 배열이 아닙니다 — 빈 store 반환.', { filePath });
return [];
}
return parsed as StocksStore;
} catch (e: any) {
logError('stocks.json 읽기 실패.', { filePath, error: e?.message ?? String(e) });
return [];
}
}
/** Atomic write — tmp + rename. 워크스페이스 없으면 false 반환 (caller 가 안내). */
export function writeStocksStore(store: StocksStore): boolean {
const filePath = getStocksFilePath();
if (!filePath) {
logError('워크스페이스 폴더가 없어 stocks.json 쓰기 불가.');
return false;
}
try {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tmp, JSON.stringify(store, null, 2), 'utf-8');
fs.renameSync(tmp, filePath);
return true;
} catch (e: any) {
logError('stocks.json 쓰기 실패.', { filePath, error: e?.message ?? String(e) });
return false;
}
}
/** 한 종목 추가. 같은 심볼이 이미 있으면 false (caller 가 안내). */
export function addStock(stock: Stock): { ok: boolean; reason?: string } {
const store = readStocksStore();
if (store.some(s => s. === stock.)) {
return { ok: false, reason: `심볼 ${stock.} 이미 존재` };
}
store.push(stock);
const wrote = writeStocksStore(store);
if (!wrote) return { ok: false, reason: '쓰기 실패 (워크스페이스 없음 또는 권한)' };
logInfo('Stocks: 종목 추가.', { symbol: stock., name: stock.이름 });
return { ok: true };
}
/** 한 종목 제거. 못 찾으면 false. */
export function removeStock(symbol: string): { ok: boolean; reason?: string } {
const store = readStocksStore();
const idx = store.findIndex(s => s. === symbol);
if (idx < 0) return { ok: false, reason: `심볼 ${symbol} 못 찾음` };
const removed = store.splice(idx, 1)[0];
const wrote = writeStocksStore(store);
if (!wrote) return { ok: false, reason: '쓰기 실패' };
logInfo('Stocks: 종목 제거.', { symbol, name: removed.이름 });
return { ok: true };
}
/** 한 종목의 필드 patch — 현재가 갱신 / 필터 업데이트 등. */
export function updateStock(symbol: string, patch: Partial<Stock>): boolean {
const store = readStocksStore();
const idx = store.findIndex(s => s. === symbol);
if (idx < 0) return false;
store[idx] = { ...store[idx], ...patch };
return writeStocksStore(store);
}