import type { LogicNode } from './strategyTypes'; import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage'; import type { StrategyEditorMode } from './strategyEditorModeStorage'; import type { StrategyDto } from './strategyEditorShared'; export const STRATEGY_EXPORT_VERSION = '1.1'; export interface StrategyExportPayload { version: string; name?: string; description?: string; buyCondition?: LogicNode | null; sellCondition?: LogicNode | null; exportedAt: string; source?: 'strategy-builder' | 'strategy-page'; flowLayout?: StrategyFlowLayoutStore; editorMode?: StrategyEditorMode; } export interface StrategyListExportItem { name: string; description?: string; buyCondition?: LogicNode | null; sellCondition?: LogicNode | null; enabled?: boolean; flowLayout?: StrategyFlowLayoutStore; } export interface StrategyListExportPayload { version: string; exportedAt: string; totalCount: number; source?: 'strategy-builder' | 'strategy-page'; strategies: StrategyListExportItem[]; } export type StrategyImportResult = | { kind: 'single'; data: StrategyExportPayload } | { kind: 'list'; data: StrategyListExportPayload }; function isLogicNode(value: unknown): value is LogicNode { return !!value && typeof value === 'object' && typeof (value as LogicNode).type === 'string'; } function hasStrategyConditions(data: Record): boolean { return isLogicNode(data.buyCondition) || isLogicNode(data.sellCondition); } export function buildStrategyExportPayload(input: { name?: string; description?: string; buyCondition?: LogicNode | null; sellCondition?: LogicNode | null; flowLayout?: StrategyFlowLayoutStore; editorMode?: StrategyEditorMode; }): StrategyExportPayload { return { version: STRATEGY_EXPORT_VERSION, source: 'strategy-builder', name: input.name, description: input.description, buyCondition: input.buyCondition ?? null, sellCondition: input.sellCondition ?? null, flowLayout: input.flowLayout, editorMode: input.editorMode, exportedAt: new Date().toISOString(), }; } export function buildStrategyListExportPayload( strategies: StrategyListExportItem[], ): StrategyListExportPayload { return { version: STRATEGY_EXPORT_VERSION, source: 'strategy-builder', exportedAt: new Date().toISOString(), totalCount: strategies.length, strategies, }; } export function parseStrategyImportFile(text: string): StrategyImportResult { let parsed: unknown; try { parsed = JSON.parse(text); } catch { throw new Error('JSON 파싱에 실패했습니다'); } if (!parsed || typeof parsed !== 'object') { throw new Error('유효하지 않은 JSON 형식입니다'); } const data = parsed as Record; if (Array.isArray(data.strategies)) { const strategies = data.strategies.filter((item): item is StrategyListExportItem => { if (!item || typeof item !== 'object') return false; const row = item as StrategyListExportItem; return !!row.name && (isLogicNode(row.buyCondition) || isLogicNode(row.sellCondition)); }); if (strategies.length === 0) { throw new Error('가져올 전략 조건이 없습니다'); } return { kind: 'list', data: { version: String(data.version ?? STRATEGY_EXPORT_VERSION), exportedAt: String(data.exportedAt ?? new Date().toISOString()), totalCount: strategies.length, source: data.source === 'strategy-page' ? 'strategy-page' : 'strategy-builder', strategies, }, }; } if (!hasStrategyConditions(data)) { throw new Error('buyCondition 또는 sellCondition이 필요합니다'); } return { kind: 'single', data: { version: String(data.version ?? '1.0'), source: data.source === 'strategy-page' ? 'strategy-page' : 'strategy-builder', name: typeof data.name === 'string' ? data.name : undefined, description: typeof data.description === 'string' ? data.description : undefined, buyCondition: isLogicNode(data.buyCondition) ? data.buyCondition : null, sellCondition: isLogicNode(data.sellCondition) ? data.sellCondition : null, flowLayout: data.flowLayout as StrategyFlowLayoutStore | undefined, editorMode: data.editorMode === 'list' || data.editorMode === 'graph' ? data.editorMode : undefined, exportedAt: String(data.exportedAt ?? new Date().toISOString()), }, }; } export function downloadStrategyJson(filename: string, data: unknown): void { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const href = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = href; anchor.download = filename.endsWith('.json') ? filename : `${filename}.json`; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(href); } export function pickJsonFile(): Promise { return new Promise(resolve => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.onchange = async () => { const file = input.files?.[0]; if (!file) { resolve(null); return; } try { resolve(await file.text()); } catch { resolve(null); } }; input.click(); }); } export function strategyDtoToListExportItem( strategy: StrategyDto, flowLayout?: StrategyFlowLayoutStore | null, ): StrategyListExportItem { return { name: strategy.name, description: strategy.description, buyCondition: strategy.buyCondition ?? null, sellCondition: strategy.sellCondition ?? null, enabled: strategy.enabled, flowLayout: flowLayout ?? undefined, }; } export function listImportItemToStrategyDto(item: StrategyListExportItem, id: number): StrategyDto { const now = new Date().toISOString(); return { id, name: item.name, description: item.description, buyCondition: item.buyCondition ?? null, sellCondition: item.sellCondition ?? null, enabled: item.enabled ?? true, createdAt: now, updatedAt: now, }; }