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): boolean { const store = readStocksStore(); const idx = store.findIndex(s => s.심볼 === symbol); if (idx < 0) return false; store[idx] = { ...store[idx], ...patch }; return writeStocksStore(store); }