/** * 전략 조건 읽기전용 뷰 — TradeAlertModal 내 "전략 조건" 탭에 표시 * LogicNode 트리를 전략 편집기와 유사한 카드 형태로 렌더링한다. */ import React from 'react'; import type { LogicNode } from '../../utils/strategyTypes'; import { CONDITION_LABEL, resolveCandleRangeMode, candleRangeWindowBars } from '../../utils/strategyTypes'; import { getStrategyIndicatorDisplayName } from '../../utils/strategyPaletteStorage'; import { getIndicatorPaletteCategory } from '../strategyEditor/paletteCategories'; import { nodeToText } from '../../utils/strategyEditorShared'; import { formatStrategyCandleLabel } from '../../utils/strategyStartNodes'; import type { StrategyDto } from '../../utils/backendApi'; /* ── nodeToText 출력에서 조건대상1 / 조건대상2 분리 ─────────────── */ /** nodeToText 가 COND_OPTIONS 라벨로 출력하는 문자열 목록 (긴 것 우선으로 체크) */ const COND_SEARCH = [ '구름 상향 돌파', '구름 하향 돌파', '선행스팬1 상향돌파 선행스팬2', '선행스팬1 하향돌파 선행스팬2', '선행스팬1 > 선행스팬2', '선행스팬1 < 선행스팬2', '후행스팬 > 가격', '후행스팬 < 가격', '상향 돌파', '하향 돌파', '상승 기울기', '하락 기울기', '이상 (>=)', '이하 (<=)', '초과 (>)', '미만 (<)', '같음 (==)', '다름 (!=)', '차이 > 값', '차이 < 값', 'N일 연속 유지', '구름 위', '구름 아래', '구름 안', ]; function splitDescByCondType(descText: string): { left: string; right: string; mid: string } { for (const cond of COND_SEARCH) { const idx = descText.indexOf(cond); if (idx > 0) { return { left: descText.slice(0, idx).trim(), mid: cond, right: descText.slice(idx + cond.length).trim(), }; } } return { left: descText, mid: '', right: '' }; } /* ── 개별 조건 카드 ────────────────────────────────────────────── */ const ConditionCard: React.FC<{ node: LogicNode }> = ({ node }) => { const cond = node.condition; if (!cond) return null; const indType = cond.indicatorType; const indLabel = getStrategyIndicatorDisplayName(indType); const cat = getIndicatorPaletteCategory(indType); const fullText = nodeToText(node); const prefix = `${indLabel} - `; const descText = fullText.startsWith(prefix) ? fullText.slice(prefix.length) : fullText; const { left, mid: condStr, right } = splitDescByCondType(descText); const rangeMode = resolveCandleRangeMode(cond); const rangeN = candleRangeWindowBars(cond); const candleRangeLabel = rangeMode === 'CURRENT' ? '현재 캔들' : rangeMode === 'EXISTS_IN' ? `최근 ${rangeN}봉 내 존재` : `최근 ${rangeN}봉 내 미존재`; const condLabelDisplay = CONDITION_LABEL[cond.conditionType] ?? (condStr || cond.conditionType); return (
{indType} {indLabel} - {descText}
캔들범위 {candleRangeLabel}
{left && (
조건대상1 {left}
)} {right && (
조건대상2 {right}
)}
조건 {condLabelDisplay}
); }; /* ── 노드 트리 재귀 렌더러 ─────────────────────────────────────── */ function renderNode(node: LogicNode): React.ReactNode { if (node.type === 'CONDITION') { return ; } if (node.type === 'AND' || node.type === 'OR') { const op = node.type as 'AND' | 'OR'; const opKo = op === 'AND' ? 'AND (그리고)' : 'OR (또는)'; const children = node.children ?? []; return (
{op} {opKo}
AND OR
{children.map(child => renderNode(child))}
); } if (node.type === 'NOT') { const inner = node.children?.[0]; return (
NOT NOT (부정)
{inner ? renderNode(inner) : 빈 NOT}
); } if (node.type === 'TIMEFRAME') { const inner = node.children?.[0]; const types = node.candleTypes?.length ? node.candleTypes : [node.candleType ?? '1m']; const tfLabel = types.map(formatStrategyCandleLabel).join(' · '); return (
START {tfLabel}
{inner ?
{renderNode(inner)}
: 빈 조건 }
); } return null; } /* ── 매수/매도 조건 트리 ─────────────────────────────────────────── */ const ConditionTree: React.FC<{ root: LogicNode | null | undefined }> = ({ root }) => { if (!root) return

조건이 없습니다.

; return
{renderNode(root)}
; }; /* ── 메인 컴포넌트 ───────────────────────────────────────────────── */ interface Props { strategy: StrategyDto; /** 알림의 신호 타입으로 초기 탭 결정 */ signalType?: 'BUY' | 'SELL'; } const StrategyConditionView: React.FC = ({ strategy, signalType }) => { const hasBuy = Boolean(strategy.buyCondition); const hasSell = Boolean(strategy.sellCondition); const showBoth = hasBuy && hasSell; const [tab, setTab] = React.useState<'buy' | 'sell'>( signalType === 'SELL' && hasSell ? 'sell' : 'buy', ); const buyRoot = strategy.buyCondition as LogicNode | null | undefined; const sellRoot = strategy.sellCondition as LogicNode | null | undefined; return (
{showBoth && (
)} {!showBoth && hasBuy && (
매수 조건 (Entry)
)} {!showBoth && hasSell && (
매도 조건 (Exit)
)}
{showBoth ? ( tab === 'buy' ? : ) : hasBuy ? ( ) : ( )}
); }; export default StrategyConditionView;