import * as vscode from 'vscode'; import { clearSheetRange, ensureSheetTab, readSheetRange, writeSheetRange, type SheetValues } from '../sheets'; import { logError, logInfo } from '../../utils'; import { onStocksStoreChanged, readStocksStore, writeStocksStore } from './stocksStore'; import type { Stock } from './types'; /** * stocks.json 원본 클라우드 백업 — Google Sheets `_backup` 탭 (v2.2.297). * * 문제: 목록의 source of truth 가 로컬 `.astra/stocks.json` 뿐이라 컴퓨터를 바꾸면 * 데이터가 사라진다. Sheet1~3 sync 는 분류 *표시용* 이라 복원 불가. * * 해결: 같은 spreadsheet(`g1nation.stocks.spreadsheetId`)에 `_backup` 탭을 자동 * 생성하고, 종목당 한 행으로 **원본 JSON 그대로** 백업. 새 컴퓨터에서는 * `/stocks restore` 한 번으로 복원. * * 백업 시점: writeStocksStore 성공 → onStocksStoreChanged → 디바운스 30초 후 1회. * (add/remove/check/discover 자동편입/워처 가격갱신 전부 이 경로를 지남.) * * 레이아웃: * A1: BACKUP_MARKER B1: ISO timestamp C1: 종목 수 * A2~: 심볼 B2~: 이름 C2~: JSON.stringify(stock) * 심볼/이름 컬럼은 사람이 시트에서 훑어보기용 — 복원은 C열 JSON 만 사용. */ const BACKUP_TAB = '_backup'; const BACKUP_MARKER = 'ASTRA_STOCKS_BACKUP_V1'; const DEBOUNCE_MS = 30_000; function getSpreadsheetId(): string { return (vscode.workspace.getConfiguration('g1nation').get('stocks.spreadsheetId') || '').trim(); } function autoBackupEnabled(): boolean { return vscode.workspace.getConfiguration('g1nation').get('stocks.autoBackup', true) !== false; } /** 지금 즉시 백업 1회. spreadsheetId 미설정이면 skip (ok:false + reason). */ export async function backupToSheet( context: vscode.ExtensionContext, ): Promise<{ ok: boolean; reason?: string; count?: number }> { const spreadsheetId = getSpreadsheetId(); if (!spreadsheetId) { return { ok: false, reason: 'g1nation.stocks.spreadsheetId 미설정 — Settings 에서 spreadsheet ID 를 넣어주세요.' }; } const store = readStocksStore(); const ensured = await ensureSheetTab(context, spreadsheetId, BACKUP_TAB); if (!ensured.ok) return { ok: false, reason: `백업 탭 확인 실패: ${ensured.error}` }; if (ensured.created) logInfo(`Stocks backup: '${BACKUP_TAB}' 탭 생성.`); // 목록이 줄었을 때 잔여 행이 남지 않게 먼저 클리어. 클리어 실패는 치명적이지 // 않으므로 (덮어쓰기는 어차피 진행) 로그만 남기고 계속. const cleared = await clearSheetRange(context, spreadsheetId, `${BACKUP_TAB}!A:C`); if (!cleared.ok) logError('Stocks backup: 기존 행 클리어 실패 — 덮어쓰기로 계속.', { error: cleared.error }); const rows: SheetValues = [[BACKUP_MARKER, new Date().toISOString(), store.length]]; for (const s of store) { rows.push([s.심볼, s.이름, JSON.stringify(s)]); } const wrote = await writeSheetRange(context, spreadsheetId, `${BACKUP_TAB}!A1`, rows); if (!wrote.ok) return { ok: false, reason: `백업 쓰기 실패: ${wrote.error}` }; logInfo('Stocks backup: 완료.', { count: store.length }); return { ok: true, count: store.length }; } /** * `_backup` 탭에서 목록 복원 — 로컬 stocks.json 을 백업 내용으로 **교체**. * silent write — 복원 직후 같은 내용을 되백업하는 루프 방지. */ export async function restoreFromSheet( context: vscode.ExtensionContext, ): Promise<{ ok: boolean; reason?: string; count?: number; backedUpAt?: string; previousCount?: number }> { const spreadsheetId = getSpreadsheetId(); if (!spreadsheetId) { return { ok: false, reason: 'g1nation.stocks.spreadsheetId 미설정 — Settings 에서 spreadsheet ID 를 넣어주세요.' }; } const read = await readSheetRange(context, spreadsheetId, `${BACKUP_TAB}!A1:C`); if (!read.ok) return { ok: false, reason: `백업 탭 읽기 실패: ${read.error} — 아직 백업한 적이 없다면 /stocks backup 먼저.` }; const values = read.values; const header = values[0] ?? []; if (String(header[0] ?? '') !== BACKUP_MARKER) { return { ok: false, reason: `'${BACKUP_TAB}' 탭이 백업 형식이 아닙니다 (헤더 마커 불일치).` }; } const backedUpAt = String(header[1] ?? ''); const restored: Stock[] = []; for (const row of values.slice(1)) { const raw = String(row[2] ?? '').trim(); if (!raw) continue; try { const s = JSON.parse(raw); if (s && typeof s === 'object' && typeof s.심볼 === 'string' && typeof s.이름 === 'string') { restored.push(s as Stock); } } catch { /* 손상 행은 skip — 나머지는 살린다 */ } } if (restored.length === 0) { return { ok: false, reason: '백업에 유효한 종목이 없습니다 — 로컬 목록을 건드리지 않았습니다.' }; } const previousCount = readStocksStore().length; if (!writeStocksStore(restored, { silent: true })) { return { ok: false, reason: 'stocks.json 쓰기 실패 (워크스페이스 확인).' }; } logInfo('Stocks restore: 완료.', { count: restored.length, backedUpAt, previousCount }); return { ok: true, count: restored.length, backedUpAt, previousCount }; } /** * 자동 백업 배선 — extension activate 에서 1회 호출. * store 변경 → 30초 디바운스 → backupToSheet. spreadsheetId 미설정/설정 OFF 면 조용히 skip. */ export function registerStocksAutoBackup(context: vscode.ExtensionContext): vscode.Disposable { let timer: NodeJS.Timeout | undefined; const sub = onStocksStoreChanged(() => { if (!autoBackupEnabled() || !getSpreadsheetId()) return; if (timer) clearTimeout(timer); timer = setTimeout(async () => { timer = undefined; const r = await backupToSheet(context); if (!r.ok) logError('Stocks 자동 백업 실패.', { reason: r.reason }); }, DEBOUNCE_MS); }); return { dispose: () => { sub.dispose(); if (timer) { clearTimeout(timer); timer = undefined; } }, }; }