Files
connectai/src/features/stocks/sheetsSync.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

100 lines
4.1 KiB
TypeScript

import * as vscode from 'vscode';
import { writeSheetRange, type SheetValues } from '../sheets';
import { logError, logInfo } from '../../utils';
import { classifyAll } from './signalClassifier';
import { readStocksStore } from './stocksStore';
import type { ClassifiedStock } from './types';
/**
* 분류된 종목 리스트 → Google Sheets 동기화.
*
* invest_results/gs_update_api.js 의 시트 레이아웃을 유지:
* - Sheet1!A1:O = 스윙/중기
* - Sheet2!A1:O = 장기투자
* - Sheet3!A1:O = 저평가우량주
* (시트 이름은 spreadsheet 의 1/2/3번째 탭 — 사용자가 본인 spreadsheet 에 미리 만들어 둠.)
*
* spreadsheet ID 는 설정 `g1nation.stocks.spreadsheetId` 에서 읽음. 미설정이면 skip 안내.
* OAuth token 은 calendar 와 공유 (oauth.ts SCOPE 에 spreadsheets 이미 포함).
*/
const HEADER = ['종목명', '심볼', '상장일', '현재가', '적정주가', '매수권장가', '3/4 필터', '매수 신호', 'ROE', 'PBR', '영업이익률', '유보율', 'PER', 'EPS', '특이사항'];
function buildSheetRows(classified: ClassifiedStock[]): SheetValues {
const rows: SheetValues = [HEADER];
for (const s of classified) {
rows.push([
s.,
s.,
s. ?? '',
s. ?? 0,
s. ?? '',
s. ?? '',
s.filterPass ? 'Pass' : 'Fail',
s.signalText,
s['ROE(25E)'] ?? '',
s.PBR ?? '',
s['영업이익률(25E)'] ?? '',
s. ?? '',
s['PER(25E)'] ?? '',
s['EPS(25E)'] ?? '',
s. ?? '',
]);
}
return rows;
}
/**
* 3 시트 일괄 동기화. 결과는 시트별 성공/실패 카운트로 반환 — caller 가 webview 에 표시.
*
* Sheet 이름은 1/2/3번째 탭 인덱스가 아니라 *이름* 으로 range 를 짜야 안전.
* 사용자가 본인 spreadsheet 의 탭 이름을 코드 기본값과 맞추도록 안내:
* 'Sheet1' / 'Sheet2' / 'Sheet3' — 또는 설정으로 override 가능.
*/
export async function syncToSheets(
context: vscode.ExtensionContext,
): Promise<{ ok: boolean; errors: string[]; updatedRanges: string[] }> {
const cfg = vscode.workspace.getConfiguration('g1nation');
const spreadsheetId = (cfg.get<string>('stocks.spreadsheetId') || '').trim();
if (!spreadsheetId) {
return {
ok: false,
errors: ['Settings 에 g1nation.stocks.spreadsheetId 가 설정되지 않았습니다.'],
updatedRanges: [],
};
}
const sheetSwing = (cfg.get<string>('stocks.sheetSwing') || 'Sheet1').trim();
const sheetLong = (cfg.get<string>('stocks.sheetLong') || 'Sheet2').trim();
const sheetUltra = (cfg.get<string>('stocks.sheetUltraLow') || 'Sheet3').trim();
const store = readStocksStore();
const groups = classifyAll(store);
const errors: string[] = [];
const updatedRanges: string[] = [];
const tasks: Array<{ tab: string; rows: SheetValues; label: string }> = [
{ tab: sheetSwing, rows: buildSheetRows(groups.swing), label: '스윙/중기' },
{ tab: sheetLong, rows: buildSheetRows(groups.long), label: '장기투자' },
{ tab: sheetUltra, rows: buildSheetRows(groups.ultraLow), label: '저평가우량주' },
];
for (const t of tasks) {
if (t.rows.length <= 1) continue;
const range = `${t.tab}!A1:O${t.rows.length}`;
try {
const r = await writeSheetRange(context, spreadsheetId, range, t.rows);
if (r.ok) {
updatedRanges.push(r.updatedRange);
logInfo(`Stocks sheets sync: ${t.label} OK.`, { range: r.updatedRange, cells: r.updatedCells });
} else {
errors.push(`${t.label}: ${r.error}`);
logError(`Stocks sheets sync: ${t.label} 실패.`, { error: r.error });
}
} catch (e: any) {
errors.push(`${t.label}: ${e?.message ?? String(e)}`);
}
}
return { ok: errors.length === 0, errors, updatedRanges };
}