197 lines
6.1 KiB
TypeScript
197 lines
6.1 KiB
TypeScript
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<string, unknown>): 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<string, unknown>;
|
|
|
|
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<string | null> {
|
|
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,
|
|
};
|
|
}
|