[add] 프론트엔드 Analysis 컨테이너 변경

This commit is contained in:
kimheesu 2025-06-13 17:07:38 +09:00
parent 3d5c4c09ab
commit 15f1ea64fd
8 changed files with 309 additions and 126 deletions

View File

@ -21,8 +21,40 @@ def create_fundamentals_analyst(llm, toolkit):
] ]
system_message = ( system_message = (
"You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, company financial history, insider sentiment and insider transactions to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." """You are a fundamental analyst. Your task is to provide a comprehensive report on a given company by analyzing its financial documents, company profile, financial history, insider sentiment, and transactions.
+ " Make sure to append a Makrdown table at the end of the report to organize key points in the report, organized and easy to read.",
You must output your findings in a structured JSON format. Do not add any text outside the JSON structure.
The JSON object must contain the following keys:
1. `company_overview`: A string with a summary of the company's business and market position.
2. `financial_performance`: An array of objects, each with `metric` and `value` keys (e.g., {"metric": "Earnings Per Share (EPS)", "value": "Increased by 354%"}).
3. `stock_market_info`: An array of objects, each with `metric` and `value` keys (e.g., {"metric": "Current Stock Price", "value": "$380.58"}).
4. `analyst_forecasts`: An array of objects, each with `metric` and `value` keys (e.g., {"metric": "Median Price Target", "value": "$538.00"}).
5. `insider_sentiment`: A string summarizing insider trading activity and sentiment.
6. `summary`: A string providing a final, overall conclusion based on all the fundamental data.
Here is an example of the expected JSON output format:
```json
{
"company_overview": "Applovin Corporation (APP)은 모바일 앱 개발 및 수익화에 특화된 기술 회사입니다. 지난 한 해 동안 괄목할 만한 재무 성과를 보여주며 시장에서 강력한 입지를 나타냈습니다.",
"financial_performance": [
{"metric": "주당 순이익 (EPS)", "value": "지난 1년간 354% 증가"},
{"metric": "매출 성장률", "value": "전년 대비 43.44% 성장"}
],
"stock_market_info": [
{"metric": "현재 주가", "value": "$380.58"},
{"metric": "전일 대비 변동", "value": "-0.74% 감소"}
],
"analyst_forecasts": [
{"metric": "중간 목표 주가", "value": "$538.00 (현재가 대비 약 75.4% 상승 가능성)"}
],
"insider_sentiment": "제공된 데이터에서는 구체적인 내부자 거래 내역이 자세히 설명되지 않았지만, 임원 및 이사회 구성원의 신뢰도에 대한 통찰력을 제공할 수 있습니다.",
"summary": "전반적인 재무 건전성은 긍정적이나, 주가 변동성을 고려할 때 신중한 접근이 필요합니다."
}
```
Please ensure all text content within the JSON is written in Korean.
"""
) )
prompt = ChatPromptTemplate.from_messages( prompt = ChatPromptTemplate.from_messages(

View File

@ -22,32 +22,65 @@ def create_market_analyst(llm, toolkit):
] ]
system_message = ( system_message = (
"""You are a trading assistant tasked with analyzing financial markets. Your role is to select the **most relevant indicators** for a given market condition or trading strategy from the following list. The goal is to choose up to **8 indicators** that provide complementary insights without redundancy. Categories and each category's indicators are: """You are a trading assistant tasked with analyzing financial markets. Your role is to select the **most relevant indicators** for a given market condition or trading strategy from the following list. The goal is to choose up to **8 indicators** that provide complementary insights without redundancy.
First, call `get_YFin_data` to retrieve the necessary stock data. Then, use `get_stockstats_indicators_report` with the selected indicators.
After analyzing the results, you must output your findings in a structured JSON format. Do not add any text outside the JSON structure.
The JSON object must contain the following keys:
1. `price_summary`: A string containing a detailed analysis of the stock's price movement (최고가, 최저가, 최근 동향 등).
2. `indicator_analysis`: An array of objects, where each object represents a technical indicator and has the following keys:
- `indicator`: The name of the indicator (e.g., "50 SMA").
- `value`: The calculated value of the indicator.
- `interpretation`: A detailed interpretation of what the indicator's value means in the current market context.
3. `overall_conclusion`: A string providing a comprehensive conclusion based on the combined analysis of price trends and technical indicators.
Here is an example of the expected JSON output format:
```json
{
"price_summary": "APP의 최근 주가는 2025년 6월 12일 기준으로 380.58 달러로 마감하였으며, 최고가는 428.99 달러(2025년 6월 5일), 최저가는 276.8 달러(2025년 5월 1일)입니다. 5월 초에 비해 급격히 상승하였으나, 최근에는 약간의 조정세를 보이고 있습니다.",
"indicator_analysis": [
{
"indicator": "50 SMA",
"value": "319.97",
"interpretation": "중기 추세 지표로, 현재 주가가 이 지표를 상회하고 있어 상승 추세를 나타냅니다."
},
{
"indicator": "MACD",
"value": "18.33",
"interpretation": "모멘텀 지표로, 양수 값을 유지하고 있어 상승 모멘텀을 나타냅니다."
}
],
"overall_conclusion": "APP의 주가는 현재 강한 상승세를 보이고 있으나, 단기 조정 가능성이 존재합니다. 따라서, 투자자들은 시장의 변동성을 고려하여 신중한 접근이 필요합니다."
}
```
Available indicators:
Moving Averages: Moving Averages:
- close_50_sma: 50 SMA: A medium-term trend indicator. Usage: Identify trend direction and serve as dynamic support/resistance. Tips: It lags price; combine with faster indicators for timely signals. - close_50_sma: 50 SMA
- close_200_sma: 200 SMA: A long-term trend benchmark. Usage: Confirm overall market trend and identify golden/death cross setups. Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries. - close_200_sma: 200 SMA
- close_10_ema: 10 EMA: A responsive short-term average. Usage: Capture quick shifts in momentum and potential entry points. Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals. - close_10_ema: 10 EMA
MACD Related: MACD Related:
- macd: MACD: Computes momentum via differences of EMAs. Usage: Look for crossovers and divergence as signals of trend changes. Tips: Confirm with other indicators in low-volatility or sideways markets. - macd: MACD
- macds: MACD Signal: An EMA smoothing of the MACD line. Usage: Use crossovers with the MACD line to trigger trades. Tips: Should be part of a broader strategy to avoid false positives. - macds: MACD Signal
- macdh: MACD Histogram: Shows the gap between the MACD line and its signal. Usage: Visualize momentum strength and spot divergence early. Tips: Can be volatile; complement with additional filters in fast-moving markets. - macdh: MACD Histogram
Momentum Indicators: Momentum Indicators:
- rsi: RSI: Measures momentum to flag overbought/oversold conditions. Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis. - rsi: RSI
Volatility Indicators: Volatility Indicators:
- boll: Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. Usage: Acts as a dynamic benchmark for price movement. Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals. - boll: Bollinger Middle
- boll_ub: Bollinger Upper Band: Typically 2 standard deviations above the middle line. Usage: Signals potential overbought conditions and breakout zones. Tips: Confirm signals with other tools; prices may ride the band in strong trends. - boll_ub: Bollinger Upper Band
- boll_lb: Bollinger Lower Band: Typically 2 standard deviations below the middle line. Usage: Indicates potential oversold conditions. Tips: Use additional analysis to avoid false reversal signals. - boll_lb: Bollinger Lower Band
- atr: ATR: Averages true range to measure volatility. Usage: Set stop-loss levels and adjust position sizes based on current market volatility. Tips: It's a reactive measure, so use it as part of a broader risk management strategy. - atr: ATR
Volume-Based Indicators: Volume-Based Indicators:
- vwma: VWMA: A moving average weighted by volume. Usage: Confirm trends by integrating price action with volume data. Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses. - vwma: VWMA
- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_YFin_data first to retrieve the CSV that is needed to generate indicators. Write a very detailed and nuanced report of the trends you observe. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions.""" Please write all text content within the JSON in Korean.
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read. Please write all responses in Korean.""" """
) )
prompt = ChatPromptTemplate.from_messages( prompt = ChatPromptTemplate.from_messages(

View File

@ -2,6 +2,8 @@
from typing import Dict, Any from typing import Dict, Any
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
import json
import re
class Reflector: class Reflector:
@ -122,44 +124,56 @@ Adhere strictly to these instructions, and ensure your output is detailed, accur
@staticmethod @staticmethod
def generate_final_report(final_state: dict) -> str: def generate_final_report(final_state: dict) -> str:
"""Generate a final, comprehensive report from the final state.""" """
Generate a final, comprehensive report from the final state, ensuring
report_parts = [] all parts are combined into a single, valid JSON object.
report_parts.append(f"# 최종 분석 보고서: {final_state.get('company_of_interest', 'N/A')}") """
report_parts.append(f"**분석 기준일:** {final_state.get('trade_date', 'N/A')}") final_report_json = {
report_parts.append("---") "company_info": {
"ticker": final_state.get('company_of_interest', 'N/A'),
"analysis_date": final_state.get('trade_date', 'N/A')
},
"reports": {},
"final_decision": {}
}
# 각 분석가 리포트 추가 def extract_json(text: str) -> dict:
if final_state.get('market_report'): """Extracts a JSON object from a string, even if it's embedded in other text."""
report_parts.append("## 시장 분석가 리포트") if not isinstance(text, str):
report_parts.append(final_state['market_report']) return {} # Return empty dict if not a string
if final_state.get('sentiment_report'): # Find the start and end of the JSON object
report_parts.append("## 소셜 미디어 분석가 리포트") match = re.search(r'\{.*\}', text, re.DOTALL)
report_parts.append(final_state['sentiment_report']) if match:
json_str = match.group(0)
try:
return json.loads(json_str)
except json.JSONDecodeError:
return {"error": "Failed to decode JSON", "original_text": json_str}
return {"error": "No JSON object found", "original_text": text}
if final_state.get('news_report'): # Process each report
report_parts.append("## 뉴스 분석가 리포트") report_keys = ['market_report', 'sentiment_report', 'news_report', 'fundamentals_report']
report_parts.append(final_state['news_report']) for key in report_keys:
if final_state.get(key):
report_name = key.replace('_report', '')
final_report_json['reports'][report_name] = extract_json(final_state[key])
if final_state.get('fundamentals_report'): # Add investment debate summary
report_parts.append("## 재무 분석가 리포트")
report_parts.append(final_state['fundamentals_report'])
# 투자 토론 요약 추가
if final_state.get('investment_debate_state'): if final_state.get('investment_debate_state'):
debate = final_state['investment_debate_state'] final_report_json['reports']['investment_debate'] = {
report_parts.append("## 투자 결정 토론 요약") "summary": final_state['investment_debate_state'].get('judge_decision', 'N/A')
report_parts.append(f"**심사위원 최종 결정:** {debate.get('judge_decision', 'N/A')}") }
# 최종 투자 계획 및 결정 추가 # Add final plan and decision
if final_state.get('investment_plan'): if final_state.get('investment_plan'):
report_parts.append("## 최종 투자 계획") final_report_json['final_decision']['investment_plan'] = final_state['investment_plan']
report_parts.append(final_state['investment_plan'])
if final_state.get('final_trade_decision'): if final_state.get('final_trade_decision'):
report_parts.append("## 최종 거래 결정") # Extract the final proposal (BUY/HOLD/SELL)
report_parts.append(final_state['final_trade_decision']) proposal_match = re.search(r'FINAL TRANSACTION PROPOSAL:\s*\*{2}(.*?)\*{2}', final_state['final_trade_decision'])
proposal = proposal_match.group(1) if proposal_match else 'N/A'
final_report_json['final_decision']['final_proposal'] = proposal
final_report_json['final_decision']['full_text'] = final_state['final_trade_decision']
report = "\n\n".join(report_parts) return json.dumps(final_report_json, ensure_ascii=False, indent=4)
return report

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
// web/frontend/src/pages/Analysis/Analysis.js // web/frontend/src/pages/Analysis/Analysis.js
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Divider, Spin, Alert, Typography } from 'antd'; import { Card, Divider, Spin, Alert, Typography, Row, Col } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import api from '../../services/api'; import api from '../../services/api';
import { useWebSocket } from '../../contexts/WebSocketContext'; import { useWebSocket } from '../../contexts/WebSocketContext';
@ -11,7 +11,7 @@ import AnalysisDisplay from './components/AnalysisDisplay';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
const AnalysisContainer = styled.div` const AnalysisContainer = styled.div`
max-width: 900px; max-width: 100%;
margin: 0 auto; margin: 0 auto;
padding: ${props => props.theme.spacing.lg}; padding: ${props => props.theme.spacing.lg};
`; `;
@ -107,7 +107,7 @@ const Analysis = () => {
관심 있는 종목에 대한 심층 분석을 시작하세요. 관심 있는 종목에 대한 심층 분석을 시작하세요.
</Paragraph> </Paragraph>
</CustomPageHeader> </CustomPageHeader>
<Divider /> <Divider style={{ margin: '16px 0' }} />
{error && !currentSessionId && ( // Show top-level error only when no session is active {error && !currentSessionId && ( // Show top-level error only when no session is active
<Alert <Alert

View File

@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
import { Card, Progress, Timeline, Button, Result, Typography, Empty, Tag } from 'antd'; import { Card, Progress, Timeline, Button, Result, Typography, Empty, Tag } from 'antd';
import { FileDoneOutlined, RedoOutlined } from '@ant-design/icons'; import { FileDoneOutlined, RedoOutlined } from '@ant-design/icons';
import styled from 'styled-components'; import styled from 'styled-components';
import ReactMarkdown from 'react-markdown'; import ReportDisplay from './ReportDisplay';
const { Title, Paragraph, Text } = Typography; const { Title, Paragraph, Text } = Typography;
@ -10,6 +10,8 @@ const DisplayCard = styled(Card)`
border-radius: ${props => props.theme.borderRadius.lg}; border-radius: ${props => props.theme.borderRadius.lg};
box-shadow: ${props => props.theme.shadows.lg}; box-shadow: ${props => props.theme.shadows.lg};
margin-top: ${props => props.theme.spacing.lg}; margin-top: ${props => props.theme.spacing.lg};
border-radius: ${props => props.theme.borderRadius.md};
background-color: ${props => props.theme.colors.backgroundSecondary};
`; `;
const TimelineContainer = styled.div` const TimelineContainer = styled.div`
@ -23,9 +25,6 @@ const TimelineContainer = styled.div`
const ReportContainer = styled.div` const ReportContainer = styled.div`
margin-top: ${props => props.theme.spacing.lg}; margin-top: ${props => props.theme.spacing.lg};
padding: ${props => props.theme.spacing.lg};
background-color: #fafafa;
border-radius: ${props => props.theme.borderRadius.md};
`; `;
const agentTagColors = { const agentTagColors = {
@ -101,8 +100,7 @@ const AnalysisDisplay = ({ sessionId, status, progress, messages, finalReport, o
{status === 'completed' && finalReport && ( {status === 'completed' && finalReport && (
<ReportContainer> <ReportContainer>
<Title level={3}>최종 분석 보고서</Title> <ReportDisplay reportData={finalReport} />
<ReactMarkdown>{finalReport}</ReactMarkdown>
</ReportContainer> </ReportContainer>
)} )}
</DisplayCard> </DisplayCard>

View File

@ -47,7 +47,7 @@ const AnalysisForm = ({ onStartAnalysis, loading }) => {
return ( return (
<FormCard> <FormCard>
<Title level={4} style={{ textAlign: 'center', marginBottom: '24px' }}> 분석 시작</Title> <Title level={4} style={{ textAlign: 'center', marginBottom: '16px' }}> 분석 시작</Title>
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
@ -124,7 +124,7 @@ const AnalysisForm = ({ onStartAnalysis, loading }) => {
</Col> </Col>
</Row> </Row>
<Form.Item style={{ marginTop: '24px' }}> <Form.Item style={{ marginTop: '16px' }}>
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"

View File

@ -1,81 +1,160 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Tabs, Typography, Divider, Tag } from 'antd'; import { Card, Tabs, Typography, Table, Tag, Row, Col, Spin, Alert } from 'antd';
import { LineChartOutlined, MessageOutlined, ReadOutlined, WalletOutlined, ProjectOutlined, ExperimentOutlined, SolutionOutlined } from '@ant-design/icons';
import styled from 'styled-components'; import styled from 'styled-components';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
const { Title, Paragraph, Text } = Typography; const { Title, Paragraph } = Typography;
const { TabPane } = Tabs;
const ReportContainer = styled(Card)` const ReportWrapper = styled.div`
margin-top: ${props => props.theme.spacing.lg}; padding: ${props => props.theme.spacing.md};
.ant-card-body { background-color: ${props => props.theme.colors.background};
padding: ${props => props.theme.spacing.xl}; `;
const SectionCard = styled(Card)`
margin-bottom: ${props => props.theme.spacing.lg};
border-radius: ${props => props.theme.borderRadius.lg};
box-shadow: ${props => props.theme.shadows.md};
& .ant-card-head {
background-color: ${props => props.theme.colors.backgroundSecondary};
} }
`; `;
const MarkdownWrapper = styled.div` const ReportDisplay = ({ reportData }) => {
h1, h2, h3 { const [parsedData, setParsedData] = useState(null);
margin-top: 24px; const [loading, setLoading] = useState(true);
margin-bottom: 16px; const [error, setError] = useState(null);
font-weight: 600;
}
h1 { font-size: 2em; }
h2 { font-size: 1.5em; border-bottom: 1px solid #f0f0f0; padding-bottom: 8px; }
h3 { font-size: 1.25em; }
p { line-height: 1.8; }
ul, ol { padding-left: 24px; }
li { margin-bottom: 8px; }
strong { color: ${props => props.theme.colors.primary}; }
`;
const reportSections = { useEffect(() => {
"시장 분석가 리포트": { icon: <LineChartOutlined />, key: 'market' }, if (reportData) {
"소셜 미디어 분석가 리포트": { icon: <MessageOutlined />, key: 'social' }, setLoading(true);
"뉴스 분석가 리포트": { icon: <ReadOutlined />, key: 'news' }, setError(null);
"재무 분석가 리포트": { icon: <WalletOutlined />, key: 'fundamentals' }, try {
"투자 결정 토론 요약": { icon: <ProjectOutlined />, key: 'debate' }, const data = JSON.parse(reportData);
"최종 투자 계획": { icon: <ExperimentOutlined />, key: 'plan' }, if (data && data.reports) {
"최종 거래 결정": { icon: <SolutionOutlined />, key: 'decision' }, setParsedData(data);
}; } else {
throw new Error("Invalid report structure received.");
}
} catch (e) {
console.error("Failed to parse report JSON:", e);
setError({
message: "보고서 파싱에 실패했습니다.",
originalData: reportData
});
} finally {
setLoading(false);
}
}
}, [reportData]);
const ReportDisplay = ({ reportContent }) => { if (loading) {
if (!reportContent) return null; return <Spin tip="보고서를 로딩 중입니다..." size="large" style={{ display: 'block', marginTop: '50px' }} />;
}
const parsedSections = {}; if (error) {
const sections = reportContent.split('## ').slice(1); return (
<SectionCard>
<Alert
message="보고서 파싱 오류"
description={error.message}
type="error"
showIcon
/>
<pre style={{ whiteSpace: 'pre-wrap', backgroundColor: '#f0f2f5', padding: '10px', borderRadius: '4px', marginTop: '16px' }}>
{error.originalData}
</pre>
</SectionCard>
);
}
if (!parsedData) {
return <Spin tip="보고서 데이터를 기다리는 중..." />;
}
sections.forEach(section => { const { company_info, reports, final_decision } = parsedData;
const lines = section.split('\n');
const title = lines[0].trim();
const content = lines.slice(1).join('\n').trim();
parsedSections[title] = content;
});
const mainTitleMatch = reportContent.match(/^# (.*)/); const renderMarketReport = (data) => {
const mainTitle = mainTitleMatch ? mainTitleMatch[1] : "최종 분석 보고서"; const { price_summary, indicator_analysis, overall_conclusion } = data;
const indicatorColumns = [
{ title: '지표', dataIndex: 'indicator', key: 'indicator', width: '20%' },
{ title: '값', dataIndex: 'value', key: 'value', width: '15%' },
{ title: '해석', dataIndex: 'interpretation', key: 'interpretation' },
];
return (
<>
<SectionCard title="가격 동향">{price_summary}</SectionCard>
<SectionCard title="기술적 지표 분석">
<Table columns={indicatorColumns} dataSource={indicator_analysis} pagination={false} size="small" rowKey="indicator" />
</SectionCard>
<SectionCard title="결론">{overall_conclusion}</SectionCard>
</>
);
};
const renderFundamentalsReport = (data) => {
const { company_overview, financial_performance, stock_market_info, analyst_forecasts, insider_sentiment, summary } = data;
const fundamentalsColumns = [
{ title: '메트릭', dataIndex: 'metric', key: 'metric', width: '40%' },
{ title: '값', dataIndex: 'value', key: 'value', width: '60%' },
];
return (
<>
<SectionCard title="회사 개요">{company_overview}</SectionCard>
<SectionCard title="재무 성과"><Table columns={fundamentalsColumns} dataSource={financial_performance} pagination={false} size="small" rowKey="metric" /></SectionCard>
<SectionCard title="주식 시장 정보"><Table columns={fundamentalsColumns} dataSource={stock_market_info} pagination={false} size="small" rowKey="metric" /></SectionCard>
<SectionCard title="애널리스트 전망"><Table columns={fundamentalsColumns} dataSource={analyst_forecasts} pagination={false} size="small" rowKey="metric" /></SectionCard>
<SectionCard title="내부자 정서 및 거래">{insider_sentiment}</SectionCard>
<SectionCard title="요약">{summary}</SectionCard>
</>
);
};
const renderGenericReport = (data) => (
<SectionCard>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
{JSON.stringify(data, null, 2)}
</pre>
</SectionCard>
);
const reportRenderers = {
'market': renderMarketReport,
'fundamentals': renderFundamentalsReport,
// Add other specific renderers here
};
const tabItems = Object.entries(reports)
.filter(([key, data]) => data && !data.error)
.map(([key, data]) => {
const renderer = reportRenderers[key] || renderGenericReport;
// Capitalize first letter for label
const label = key.charAt(0).toUpperCase() + key.slice(1).replace('_', ' ');
return {
key: key,
label: `${label} 리포트`,
children: renderer(data),
};
});
const getTagColor = (proposal) => {
switch (proposal?.toUpperCase()) {
case 'BUY': return 'green';
case 'SELL': return 'red';
case 'HOLD': return 'blue';
default: return 'default';
}
}
return ( return (
<ReportContainer> <ReportWrapper>
<Title level={2} style={{ textAlign: 'center' }}>{mainTitle}</Title> <Title level={2}>최종 분석 보고서: {company_info.ticker}</Title>
<Divider /> <Paragraph type="secondary">분석 기준일: {company_info.analysis_date}</Paragraph>
<Tabs defaultActiveKey="market"> <Row align="middle" gutter={16} style={{ marginBottom: 24 }}>
{Object.entries(parsedSections).map(([title, content]) => { <Col><Title level={4} style={{ margin: 0 }}>최종 거래 제안:</Title></Col>
const sectionInfo = Object.values(reportSections).find(info => title.includes(Object.keys(reportSections).find(key => reportSections[key] === info))) || {}; <Col><Tag color={getTagColor(final_decision.final_proposal)} style={{ fontSize: '18px', padding: '6px 12px' }}>{final_decision.final_proposal}</Tag></Col>
return ( </Row>
<TabPane
tab={<span>{sectionInfo.icon} {title}</span>} <Tabs defaultActiveKey={tabItems[0]?.key} items={tabItems} />
key={sectionInfo.key || title} </ReportWrapper>
>
<MarkdownWrapper>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</MarkdownWrapper>
</TabPane>
);
})}
</Tabs>
</ReportContainer>
); );
}; };