diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx
index bfcf43e..bc34666 100644
--- a/frontend/src/components/StrategyEditorPage.tsx
+++ b/frontend/src/components/StrategyEditorPage.tsx
@@ -107,6 +107,7 @@ import {
type StrategyFlowLayoutStore,
} from '../utils/strategyEditorLayoutStorage';
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
+import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative';
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
import StrategyListEditor from './strategyEditor/StrategyListEditor';
import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
@@ -178,7 +179,7 @@ const RIGHT_PANEL_MAX = 560;
const RIGHT_PANEL_DEFAULT = 380;
const TERMINAL_MIN = 88;
const TERMINAL_MAX = 420;
-const TERMINAL_DEFAULT = 140;
+const TERMINAL_DEFAULT = 220;
function readRightPanelWidth(): number {
return clampPanelSize(
@@ -2088,15 +2089,33 @@ export default function StrategyEditorPage({
/>
diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx
index 97c4575..7623404 100644
--- a/frontend/src/components/StrategyEvaluationPage.tsx
+++ b/frontend/src/components/StrategyEvaluationPage.tsx
@@ -4,6 +4,7 @@
import '../styles/backtestDashboard.css';
import '../styles/virtualTradingDashboard.css';
import '../styles/strategyEditorTheme.css';
+import '../styles/strategyEditor.css';
import '../styles/strategyEvaluation.css';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { Theme, Timeframe } from '../types';
@@ -20,6 +21,7 @@ import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicat
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel';
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
+import StrategyEvaluationConditionPanel from './strategyEvaluation/StrategyEvaluationConditionPanel';
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
@@ -484,6 +486,11 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
evalLoading={evalLoading}
onSignalsChange={handleSignalsChange}
/>
+
diff --git a/frontend/src/components/strategyEditor/LogicExpressionNarrative.tsx b/frontend/src/components/strategyEditor/LogicExpressionNarrative.tsx
new file mode 100644
index 0000000..117bb51
--- /dev/null
+++ b/frontend/src/components/strategyEditor/LogicExpressionNarrative.tsx
@@ -0,0 +1,104 @@
+/**
+ * LOGIC EXPRESSION 아래 — 조건을 알기 쉬운 문장으로 설명
+ */
+import React, { useMemo } from 'react';
+import type { LogicNode } from '../../utils/strategyTypes';
+import type { DefType } from '../../utils/strategyEditorShared';
+import {
+ buildStrategyNarrative,
+ type StrategyDescriptionInput,
+} from '../../utils/strategyDescriptionNarrative';
+import type { EditorConditionState } from '../../utils/strategyConditionSerde';
+import { emptyEditorConditionState } from '../../utils/strategyConditionSerde';
+
+interface Props {
+ name?: string;
+ description?: string;
+ buyCondition: LogicNode | null;
+ sellCondition: LogicNode | null;
+ buyEditorState?: EditorConditionState;
+ sellEditorState?: EditorConditionState;
+ orphanCount?: number;
+ def: DefType;
+ /** 터미널·하단 패널 등 좁은 공간 — 서두·각주 생략, 핵심 bullet 위주 */
+ compact?: boolean;
+ className?: string;
+}
+
+export default function LogicExpressionNarrative({
+ name,
+ description,
+ buyCondition,
+ sellCondition,
+ buyEditorState,
+ sellEditorState,
+ orphanCount,
+ def,
+ compact = false,
+ className,
+}: Props) {
+ const input: StrategyDescriptionInput = useMemo(() => ({
+ name,
+ description,
+ buyEditorState: buyEditorState ?? emptyEditorConditionState(),
+ sellEditorState: sellEditorState ?? emptyEditorConditionState(),
+ buyCondition,
+ sellCondition,
+ orphanCount,
+ def,
+ }), [
+ name,
+ description,
+ buyEditorState,
+ sellEditorState,
+ buyCondition,
+ sellCondition,
+ orphanCount,
+ def,
+ ]);
+
+ const narrative = useMemo(() => buildStrategyNarrative(input), [input]);
+
+ const rootClass = ['se-narrative', compact ? 'se-narrative--compact' : '', className]
+ .filter(Boolean)
+ .join(' ');
+
+ return (
+
+ {!compact && narrative.intro.map((p, i) => (
+
{p}
+ ))}
+
+ {narrative.sections.map(section => (
+
+ {section.title}
+ {!compact && section.paragraphs.map((p, i) => (
+ {p}
+ ))}
+ {compact && section.paragraphs.length > 0 && (
+
+ {section.paragraphs[section.paragraphs.length - 1]}
+
+ )}
+ {section.bullets && section.bullets.length > 0 && (
+
+ {section.bullets.map((line, i) => (
+ - {line}
+ ))}
+
+ )}
+
+ ))}
+
+ {narrative.footnotes.length > 0 && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/strategyEvaluation/StrategyEvaluationConditionPanel.tsx b/frontend/src/components/strategyEvaluation/StrategyEvaluationConditionPanel.tsx
new file mode 100644
index 0000000..4846be5
--- /dev/null
+++ b/frontend/src/components/strategyEvaluation/StrategyEvaluationConditionPanel.tsx
@@ -0,0 +1,92 @@
+/**
+ * 전략 평가 — 중앙 하단 LOGIC EXPRESSION + 조건 설명
+ */
+import React, { useCallback, useMemo } from 'react';
+import type { StrategyDto } from '../../utils/backendApi';
+import { asLogicNode } from '../../utils/strategyHydrate';
+import { decodeConditionForEditor } from '../../utils/strategyConditionSerde';
+import { buildDefFromGetParams } from '../../utils/strategyEditorShared';
+import LogicExpressionPreview from '../strategyEditor/LogicExpressionPreview';
+import LogicExpressionNarrative from '../strategyEditor/LogicExpressionNarrative';
+import type { EvalIndicatorParams } from '../../utils/strategyEvaluationParams';
+
+interface Props {
+ strategy: StrategyDto | null;
+ getParams: (type: string) => Record;
+ appliedParams: EvalIndicatorParams | null;
+}
+
+const StrategyEvaluationConditionPanel: React.FC = ({
+ strategy,
+ getParams,
+ appliedParams,
+}) => {
+ const resolveGetParams = useCallback(
+ (type: string, defaults?: Record) => {
+ const base = getParams(type);
+ const merged = defaults ? { ...defaults, ...base } : base;
+ const override = appliedParams?.[type];
+ return override ? { ...merged, ...override } : merged;
+ },
+ [getParams, appliedParams],
+ );
+
+ const def = useMemo(() => buildDefFromGetParams(resolveGetParams), [resolveGetParams]);
+
+ const buyCondition = useMemo(
+ () => (strategy ? asLogicNode(strategy.buyCondition) : null),
+ [strategy?.buyCondition],
+ );
+ const sellCondition = useMemo(
+ () => (strategy ? asLogicNode(strategy.sellCondition) : null),
+ [strategy?.sellCondition],
+ );
+
+ const buyEditorState = useMemo(
+ () => decodeConditionForEditor(buyCondition),
+ [buyCondition],
+ );
+ const sellEditorState = useMemo(
+ () => decodeConditionForEditor(sellCondition),
+ [sellCondition],
+ );
+
+ if (!strategy || (!buyCondition && !sellCondition)) {
+ return (
+
+
전략을 선택하면 조건식과 설명이 표시됩니다.
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default StrategyEvaluationConditionPanel;
diff --git a/frontend/src/styles/strategyEditor.css b/frontend/src/styles/strategyEditor.css
index 4581423..f8c4206 100644
--- a/frontend/src/styles/strategyEditor.css
+++ b/frontend/src/styles/strategyEditor.css
@@ -1552,7 +1552,18 @@
border-radius: 0;
background: var(--se-terminal-bg);
overflow: hidden;
- min-height: 88px;
+ min-height: 120px;
+}
+.se-terminal-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+}
+.se-terminal-section + .se-terminal-section {
+ border-top: 1px solid var(--se-border);
+}
+.se-terminal-section--narrative {
+ background: color-mix(in srgb, var(--se-terminal-bg) 88%, var(--se-gold) 12%);
}
.se-terminal-label {
flex-shrink: 0;
@@ -1563,6 +1574,9 @@
padding: 6px 12px;
border-bottom: 1px solid var(--se-border);
}
+.se-terminal-label--guide {
+ color: color-mix(in srgb, var(--se-gold) 85%, var(--se-text) 15%);
+}
.se-terminal-text {
flex: 1;
min-height: 0;
@@ -2626,3 +2640,64 @@ body.se-palette-drag-armed .se-palette-section--scroll {
color: var(--se-text-muted);
line-height: 1.5;
}
+
+/* ── Terminal narrative (조건 설명) ── */
+.se-narrative {
+ padding: 10px 14px 12px;
+ font-size: 0.8rem;
+ line-height: 1.55;
+ color: var(--se-text);
+}
+.se-narrative--compact {
+ padding: 8px 14px 10px;
+ font-size: 0.78rem;
+}
+.se-narrative-intro {
+ margin: 0 0 8px;
+ color: var(--se-text-muted);
+}
+.se-narrative-section + .se-narrative-section {
+ margin-top: 10px;
+ padding-top: 8px;
+ border-top: 1px dashed var(--se-border);
+}
+.se-narrative-section-title {
+ margin: 0 0 6px;
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ color: var(--se-gold);
+}
+.se-narrative-para {
+ margin: 0 0 6px;
+ color: var(--se-text-muted);
+}
+.se-narrative-para--lead {
+ font-size: 0.76rem;
+}
+.se-narrative-list {
+ margin: 4px 0 0;
+ padding: 0 0 0 1.1em;
+ list-style: none;
+}
+.se-narrative-list-item {
+ margin: 3px 0;
+ padding-left: 2px;
+ white-space: pre-wrap;
+ word-break: keep-all;
+ color: var(--se-text);
+}
+.se-narrative-footnotes {
+ margin-top: 8px;
+ padding-top: 6px;
+ border-top: 1px dashed var(--se-border);
+}
+.se-narrative-footnote {
+ margin: 3px 0 0;
+ font-size: 0.72rem;
+ color: var(--se-text-muted);
+ line-height: 1.45;
+}
+.se-narrative--compact .se-narrative-footnotes {
+ display: none;
+}
diff --git a/frontend/src/styles/strategyEvaluation.css b/frontend/src/styles/strategyEvaluation.css
index ed7f614..527e114 100644
--- a/frontend/src/styles/strategyEvaluation.css
+++ b/frontend/src/styles/strategyEvaluation.css
@@ -428,6 +428,61 @@
font-variant-numeric: tabular-nums;
}
+/* ── 중앙 하단 — LOGIC EXPRESSION + 조건 설명 ── */
+.seval-condition-panel {
+ flex-shrink: 0;
+ max-height: min(38vh, 320px);
+ margin-top: 6px;
+ border: 1px solid var(--se-terminal-border, rgba(255, 255, 255, 0.08));
+ border-radius: 8px;
+ background: var(--se-terminal-bg, color-mix(in srgb, var(--bg) 92%, #000));
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+.seval-condition-panel--empty {
+ max-height: none;
+ padding: 12px 14px;
+}
+.seval-condition-panel-empty {
+ margin: 0;
+ font-size: 0.8rem;
+ color: var(--se-text-muted);
+}
+.seval-condition-panel-section {
+ flex-shrink: 0;
+ min-height: 0;
+ overflow-y: auto;
+}
+.seval-condition-panel-section--narrative {
+ flex: 1;
+ min-height: 0;
+ border-top: 1px solid var(--se-border);
+ background: color-mix(in srgb, var(--se-terminal-bg, var(--bg)) 88%, var(--se-gold, #c9a227) 12%);
+}
+.seval-condition-panel-label {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ font-size: 0.62rem;
+ font-weight: 800;
+ letter-spacing: 0.14em;
+ color: var(--se-terminal-label, var(--se-text-muted));
+ padding: 6px 12px;
+ border-bottom: 1px solid var(--se-border);
+ background: inherit;
+}
+.seval-condition-panel-label--guide {
+ color: color-mix(in srgb, var(--se-gold, #c9a227) 85%, var(--se-text) 15%);
+}
+.seval-condition-panel .se-terminal-text {
+ max-height: 120px;
+}
+.seval-condition-panel .se-narrative--compact {
+ max-height: 160px;
+ overflow-y: auto;
+}
+
/* ── 세로 선택바 ─────────────────────────────────────────────────────────── */
.seval-bar-selector-layer {
position: absolute;