v2.2.297: stocks 목록 클라우드 백업 — Sheets _backup 탭 자동 백업 + /stocks backup·restore

로컬 .astra/stocks.json 뿐이던 워치리스트를 spreadsheet _backup 탭에 원본
JSON 그대로 자동 백업(변경 시 30초 디바운스). 새 컴퓨터에서는 /stocks
restore 한 번으로 복원. sheetsApi 에 ensureSheetTab/clearSheetRange 추가,
writeStocksStore 성공 시 변경 리스너 발화로 모든 변경 경로 포착.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 22:48:59 +09:00
co-authored by Claude Fable 5
parent 26cde3afb0
commit 443488a3b6
9 changed files with 281 additions and 4 deletions
+30
View File
@@ -9,6 +9,7 @@ import { AIService } from '../../core/services';
import { classifyAll } from './signalClassifier';
import { writeStocksStore } from './stocksStore';
import { syncToSheets } from './sheetsSync';
import { backupToSheet, restoreFromSheet } from './backupSync';
import { judgeStock } from './llmJudge';
import { sendStocksReport, buildReportText } from './telegramReport';
import { runOnceNow } from './stocksWatcher';
@@ -27,6 +28,8 @@ import type { ClassifiedStock, Stock } from './types';
* /stocks check — 현재가 갱신 (Yahoo)
* /stocks signal — 매수사정권 종목만
* /stocks sync — Google Sheets 동기화
* /stocks backup — 원본 목록을 Sheets `_backup` 탭에 백업
* /stocks restore — `_backup` 탭에서 목록 복원 (기기 이전용)
* /stocks add <심볼> <이름> [투자성향]
* /stocks remove <심볼>
* /stocks judge <심볼> — LLM 4-criteria 평가
@@ -116,6 +119,25 @@ async function cmdSync(view: Webview | undefined, context: vscode.ExtensionConte
}
}
async function cmdBackup(view: Webview | undefined, context: vscode.ExtensionContext): Promise<void> {
chunk(view, '\n☁️ Google Sheets `_backup` 탭으로 백업 중...\n');
const r = await backupToSheet(context);
chunk(view, r.ok
? `\n✅ 백업 완료: ${r.count}개 종목. 새 컴퓨터에서는 \`/stocks restore\` 로 복원하세요.\n`
: `\n❌ 백업 실패: ${r.reason}\n`);
}
async function cmdRestore(view: Webview | undefined, context: vscode.ExtensionContext): Promise<void> {
chunk(view, '\n☁️ Google Sheets `_backup` 탭에서 복원 중...\n');
const r = await restoreFromSheet(context);
if (!r.ok) {
chunk(view, `\n❌ 복원 실패: ${r.reason}\n`);
return;
}
const when = r.backedUpAt ? ` (백업 시각: ${r.backedUpAt})` : '';
chunk(view, `\n✅ 복원 완료: ${r.count}개 종목${when} — 기존 로컬 ${r.previousCount}개를 교체했습니다.\n`);
}
async function cmdAdd(arg: string, view: Webview | undefined): Promise<void> {
const parts = arg.split(/\s+/);
if (parts.length < 2) {
@@ -645,6 +667,8 @@ function cmdHelp(view: Webview | undefined): void {
' `/stocks check` — 현재가 갱신',
' `/stocks signal` — 매수사정권 종목만',
' `/stocks sync` — Google Sheets 동기화',
' `/stocks backup` — 목록 원본을 Sheets `_backup` 탭에 백업 (변경 시 자동 백업도 됨)',
' `/stocks restore` — `_backup` 탭에서 목록 복원 (새 컴퓨터 이전용)',
' `/stocks add <심볼> <이름>` — 종목 추가 (예: `add 005930 삼성전자` / `add AAPL Apple`)',
' `/stocks remove <심볼>` — 종목 제거',
' `/stocks judge <심볼>` — LLM 4-criteria 평가 (stocks.json 등록 종목)',
@@ -683,6 +707,12 @@ export async function handleStocksCommand(
case 'sync':
if (!context) { chunk(view, '\n❌ ExtensionContext 없음 (sync 불가).\n'); return true; }
await cmdSync(view, context); return true;
case 'backup':
if (!context) { chunk(view, '\n❌ ExtensionContext 없음 (backup 불가).\n'); return true; }
await cmdBackup(view, context); return true;
case 'restore':
if (!context) { chunk(view, '\n❌ ExtensionContext 없음 (restore 불가).\n'); return true; }
await cmdRestore(view, context); return true;
case 'add': await cmdAdd(rest, view); return true;
case 'remove': case 'rm': await cmdRemove(rest, view); return true;
case 'judge': await cmdJudge(rest, view); return true;