AI 어드바이저 탭을 프롬프트 생성/복사 방식으로 전환
Gemini API 직접 호출 대신 포트폴리오 데이터 기반 전문가 프롬프트를 자동 생성하고 클립보드에 복사하는 방식으로 변경. - 보유 종목, 평균매입가, 현재가, 손익, 예수금, 시장 지표 포함 - Gemini/ChatGPT 바로가기 링크 제공 - 프롬프트 미리보기 영역 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2740,6 +2740,39 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.advisor-prompt__toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advisor-prompt__info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary.small.is-copied {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advisor-prompt__preview {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
max-height: 520px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.advisor-panel__timestamp {
|
.advisor-panel__timestamp {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
addSellHistory,
|
addSellHistory,
|
||||||
updateSellHistory,
|
updateSellHistory,
|
||||||
deleteSellHistory,
|
deleteSellHistory,
|
||||||
getAiAnalysis,
|
|
||||||
} from '../../api';
|
} from '../../api';
|
||||||
import Loading from '../../components/Loading';
|
import Loading from '../../components/Loading';
|
||||||
import './Stock.css';
|
import './Stock.css';
|
||||||
@@ -92,38 +91,6 @@ const toNumeric = (value) => {
|
|||||||
return Number.isNaN(numeric) ? null : numeric;
|
return Number.isNaN(numeric) ? null : numeric;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── AdvisorMarkdown — 경량 마크다운 렌더러 ──────────────────────── */
|
|
||||||
|
|
||||||
const AdvisorMarkdown = ({ text }) => {
|
|
||||||
if (!text) return null;
|
|
||||||
const lines = text.split('\n');
|
|
||||||
const elements = [];
|
|
||||||
let i = 0;
|
|
||||||
while (i < lines.length) {
|
|
||||||
const line = lines[i];
|
|
||||||
if (line.startsWith('### ')) {
|
|
||||||
elements.push(<h4 key={i} className="adv-md-h3">{line.slice(4)}</h4>);
|
|
||||||
} else if (line.startsWith('## ')) {
|
|
||||||
elements.push(<h3 key={i} className="adv-md-h2">{line.slice(3)}</h3>);
|
|
||||||
} else if (line.startsWith('---')) {
|
|
||||||
elements.push(<hr key={i} className="adv-md-hr" />);
|
|
||||||
} else if (line.trim() === '') {
|
|
||||||
elements.push(<div key={i} className="adv-md-gap" />);
|
|
||||||
} else {
|
|
||||||
// 인라인 마크다운: **bold**, *italic*, 🎯 등 이모지 보존
|
|
||||||
const rendered = line
|
|
||||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
||||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
||||||
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
||||||
elements.push(
|
|
||||||
<p key={i} className="adv-md-p" dangerouslySetInnerHTML={{ __html: rendered }} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return <div className="adv-md">{elements}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ── Chart colors ──────────────────────────────────────────────── */
|
/* ── Chart colors ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
const CHART_COLORS = ['#818cf8', '#fbbf24', '#34d399', '#f472b6', '#fb923c', '#a78bfa', '#38bdf8', '#4ade80'];
|
const CHART_COLORS = ['#818cf8', '#fbbf24', '#34d399', '#f472b6', '#fb923c', '#a78bfa', '#38bdf8', '#4ade80'];
|
||||||
@@ -237,10 +204,8 @@ const StockTrade = () => {
|
|||||||
const [sellFormSaving, setSellFormSaving] = useState(false);
|
const [sellFormSaving, setSellFormSaving] = useState(false);
|
||||||
const [sellFormError, setSellFormError] = useState('');
|
const [sellFormError, setSellFormError] = useState('');
|
||||||
|
|
||||||
/* AI 전문가 분석 (Gemini Pro Advisor) */
|
/* AI 어드바이저 — 프롬프트 복사 */
|
||||||
const [advisorData, setAdvisorData] = useState(null); // { analysis, generated_at, cached, holdings_count }
|
const [advisorCopied, setAdvisorCopied] = useState(false);
|
||||||
const [advisorLoading, setAdvisorLoading] = useState(false);
|
|
||||||
const [advisorError, setAdvisorError] = useState('');
|
|
||||||
|
|
||||||
/* Cash (예수금) form */
|
/* Cash (예수금) form */
|
||||||
const [cashForm, setCashForm] = useState({ broker: '', cash: '' });
|
const [cashForm, setCashForm] = useState({ broker: '', cash: '' });
|
||||||
@@ -322,19 +287,85 @@ const StockTrade = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadAdvisorAnalysis = useCallback(async (force = false) => {
|
/* AI 어드바이저: 포트폴리오 기반 프롬프트 생성 */
|
||||||
setAdvisorLoading(true);
|
const buildAdvisorPrompt = useCallback(() => {
|
||||||
setAdvisorError('');
|
const today = new Date().toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
try {
|
|
||||||
const data = await getAiAnalysis(force);
|
const holdingsLines = portfolioHoldings.map((h) => {
|
||||||
if (data?.error) throw new Error(data.error);
|
const cp = h.current_price != null ? `${formatNumber(h.current_price)}원` : '시세 미조회';
|
||||||
setAdvisorData(data);
|
const rate = h.profit_rate != null ? formatPercent(h.profit_rate) : '미조회';
|
||||||
} catch (err) {
|
const profit = h.profit_amount != null ? `(${h.profit_amount >= 0 ? '+' : ''}${formatNumber(h.profit_amount)}원)` : '';
|
||||||
setAdvisorError(err?.message ?? '분석 중 오류가 발생했습니다.');
|
return `- **${h.name ?? h.ticker}** (${h.ticker ?? ''}) | 계좌: ${h.broker ?? '-'}
|
||||||
} finally {
|
수량 ${h.quantity}주 | 평균매입가 ${formatNumber(h.avg_price)}원 | 현재가 ${cp} | 손익 ${rate} ${profit}`;
|
||||||
setAdvisorLoading(false);
|
}).join('\n');
|
||||||
}
|
|
||||||
}, []);
|
const cashLines = cashList.map((c) => `- ${c.broker}: ${formatNumber(c.cash)}원`).join('\n') || '- 없음';
|
||||||
|
|
||||||
|
const marketLines = marketCtx
|
||||||
|
? [
|
||||||
|
`VIX: ${marketCtx.vix != null ? `${marketCtx.vix} (${getVixLabel(marketCtx.vix)})` : '데이터 없음'}`,
|
||||||
|
`공포탐욕지수: ${marketCtx.fg != null ? `${marketCtx.fg}점 (${getFgLabel(marketCtx.fg)})` : '데이터 없음'}`,
|
||||||
|
`미 10년물 국채: ${marketCtx.treasury != null ? `${marketCtx.treasury}%` : '데이터 없음'}`,
|
||||||
|
`WTI 유가: ${marketCtx.wti != null ? `$${marketCtx.wti}` : '데이터 없음'}`,
|
||||||
|
].join('\n')
|
||||||
|
: '시장 데이터 미로드';
|
||||||
|
|
||||||
|
return `당신은 15년 이상 경력의 한국 주식시장 전문 애널리스트입니다.
|
||||||
|
오늘은 ${today}입니다. 아래 포트폴리오 정보와 시장 환경을 바탕으로 전문가 분석을 제공해주세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 현재 시장 환경
|
||||||
|
|
||||||
|
${marketLines}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💼 보유 포트폴리오
|
||||||
|
|
||||||
|
### 보유 종목 (${portfolioHoldings.length}개)
|
||||||
|
|
||||||
|
${holdingsLines || '보유 종목 없음'}
|
||||||
|
|
||||||
|
### 포트폴리오 요약
|
||||||
|
|
||||||
|
- 총 매입금액: ${formatNumber(portfolioSummary.total_buy)}원
|
||||||
|
- 총 평가금액: ${formatNumber(portfolioSummary.total_eval)}원
|
||||||
|
- 총 손익: ${formatNumber(portfolioSummary.total_profit)}원 (수익률: ${formatPercent(portfolioSummary.total_profit_rate)})
|
||||||
|
- 예수금 합계: ${totalCash != null ? formatNumber(totalCash) + '원' : '미입력'}
|
||||||
|
- 총 자산: ${totalAssets != null ? formatNumber(totalAssets) + '원' : '미집계'}
|
||||||
|
|
||||||
|
### 예수금 현황
|
||||||
|
|
||||||
|
${cashLines}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 분석 요청
|
||||||
|
|
||||||
|
다음 형식으로 명확하게 작성해주세요:
|
||||||
|
|
||||||
|
### 📈 오늘의 시장 환경
|
||||||
|
시장 환경 데이터를 바탕으로 오늘 한국 주식시장의 전반적인 분위기와 주요 이슈를 2-3문장으로 요약하세요.
|
||||||
|
|
||||||
|
### 🔍 종목별 분석 및 행동 지침
|
||||||
|
각 보유 종목에 대해 아래 형식으로 작성하세요:
|
||||||
|
|
||||||
|
**[종목명 (티커)]**
|
||||||
|
- 현황: 현재 손익 상태와 포지션 평가
|
||||||
|
- 분석: 업황·섹터 동향, 주요 리스크/기회
|
||||||
|
- 🎯 행동 지침: **[매도 / 보유 / 추가매수 / 분할매도]** — 구체적 이유와 목표 참고 가격대
|
||||||
|
|
||||||
|
### 💼 포트폴리오 종합 의견
|
||||||
|
전체 포트폴리오의 섹터 편중, 리밸런싱 필요 여부, 현금 비중 조언을 작성하세요.
|
||||||
|
|
||||||
|
### ⚠️ 오늘 주의해야 할 리스크
|
||||||
|
매크로·섹터·개별 종목 측면에서 오늘 특히 주의할 리스크를 2-3가지 나열하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
분석은 반드시 한국어로, 구체적인 수치와 근거를 들어 전문적으로 작성해주세요.
|
||||||
|
투자 결정은 최종적으로 투자자 본인이 판단함을 명시하세요.`;
|
||||||
|
}, [portfolioHoldings, portfolioSummary, cashList, totalCash, totalAssets, marketCtx]);
|
||||||
|
|
||||||
const loadBalance = useCallback(async () => {
|
const loadBalance = useCallback(async () => {
|
||||||
setBalanceLoading(true);
|
setBalanceLoading(true);
|
||||||
@@ -414,10 +445,10 @@ const StockTrade = () => {
|
|||||||
loadBalance();
|
loadBalance();
|
||||||
} else if (activeTab === TAB_REPORT && !portfolioLoaded) {
|
} else if (activeTab === TAB_REPORT && !portfolioLoaded) {
|
||||||
loadPortfolio();
|
loadPortfolio();
|
||||||
} else if (activeTab === TAB_ADVISOR && !advisorData) {
|
} else if (activeTab === TAB_ADVISOR && !portfolioLoaded) {
|
||||||
loadAdvisorAnalysis();
|
loadPortfolio();
|
||||||
}
|
}
|
||||||
}, [activeTab, portfolioLoaded, balanceLoaded, advisorData, loadPortfolio, loadBalance, loadSellHistory, loadAdvisorAnalysis]);
|
}, [activeTab, portfolioLoaded, balanceLoaded, loadPortfolio, loadBalance, loadSellHistory]);
|
||||||
|
|
||||||
/* 자산 추이: 포트폴리오 탭 첫 진입 또는 기간 변경 시 로드 */
|
/* 자산 추이: 포트폴리오 탭 첫 진입 또는 기간 변경 시 로드 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2046,78 +2077,84 @@ ${holdingsText}${marketText}
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ════════════════════════════════════════════════════════
|
{/* ════════════════════════════════════════════════════════
|
||||||
TAB 4: AI 어드바이저 (Gemini Pro)
|
TAB 4: AI 어드바이저 — 프롬프트 생성/복사
|
||||||
════════════════════════════════════════════════════════ */}
|
════════════════════════════════════════════════════════ */}
|
||||||
{activeTab === TAB_ADVISOR && (
|
{activeTab === TAB_ADVISOR && (
|
||||||
<section className="stock-panel stock-panel--wide advisor-panel">
|
<section className="stock-panel stock-panel--wide advisor-panel">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="advisor-panel__head">
|
<div className="advisor-panel__head">
|
||||||
<div className="advisor-panel__title-block">
|
<div className="advisor-panel__title-block">
|
||||||
<span className="advisor-panel__badge">Gemini Pro</span>
|
<span className="advisor-panel__badge">AI 어드바이저</span>
|
||||||
<h3 className="advisor-panel__title">AI 전문가 분석</h3>
|
<h3 className="advisor-panel__title">포트폴리오 분석 프롬프트</h3>
|
||||||
<p className="advisor-panel__sub">
|
<p className="advisor-panel__sub">
|
||||||
보유 종목 현재가와 오늘의 뉴스를 기반으로 전문가 관점의 매매 조언을 제공합니다.
|
보유 종목 정보를 담은 전문가용 프롬프트를 생성합니다.
|
||||||
|
복사 후 Gemini, ChatGPT 등에 붙여넣어 분석을 받아보세요.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="advisor-panel__actions">
|
<div className="advisor-panel__actions">
|
||||||
{advisorData && (
|
<a
|
||||||
<span className="advisor-panel__timestamp">
|
href="https://gemini.google.com"
|
||||||
{advisorData.cached ? '🗂 캐시' : '✨ 신규'} ·{' '}
|
target="_blank"
|
||||||
{new Date(advisorData.generated_at).toLocaleTimeString('ko-KR', {
|
rel="noopener noreferrer"
|
||||||
hour: '2-digit', minute: '2-digit',
|
|
||||||
})} 기준
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="button ghost small"
|
className="button ghost small"
|
||||||
onClick={() => loadAdvisorAnalysis(true)}
|
|
||||||
disabled={advisorLoading}
|
|
||||||
>
|
>
|
||||||
{advisorLoading ? '분석 중...' : '🔄 새로 분석'}
|
Gemini 열기 ↗
|
||||||
</button>
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://chatgpt.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="button ghost small"
|
||||||
|
>
|
||||||
|
ChatGPT 열기 ↗
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 로딩 */}
|
{portfolioLoading && (
|
||||||
{advisorLoading && (
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 24 }}>
|
||||||
<div className="advisor-panel__loading">
|
<Loading type="spinner" message="포트폴리오 로딩 중..." />
|
||||||
<div className="advisor-loading-spinner" />
|
|
||||||
<div className="advisor-loading-text">
|
|
||||||
<p>Gemini Pro가 포트폴리오를 분석 중입니다...</p>
|
|
||||||
<p className="advisor-loading-sub">현재가 조회 · 뉴스 분석 · 전략 수립</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 에러 */}
|
{!portfolioLoading && portfolioHoldings.length === 0 && (
|
||||||
{!advisorLoading && advisorError && (
|
|
||||||
<div className="advisor-panel__error">
|
|
||||||
<span>⚠️ {advisorError}</span>
|
|
||||||
<button className="button ghost small" onClick={() => loadAdvisorAnalysis(true)}>
|
|
||||||
다시 시도
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 분석 결과 */}
|
|
||||||
{!advisorLoading && !advisorError && advisorData && (
|
|
||||||
<div className="advisor-panel__body">
|
|
||||||
<AdvisorMarkdown text={advisorData.analysis} />
|
|
||||||
<p className="advisor-panel__disclaimer">
|
|
||||||
※ 이 분석은 AI가 생성한 참고 자료이며 투자 결정은 본인의 판단과 책임 하에 이루어져야 합니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 초기 상태 */}
|
|
||||||
{!advisorLoading && !advisorError && !advisorData && (
|
|
||||||
<div className="advisor-panel__empty">
|
<div className="advisor-panel__empty">
|
||||||
<span className="advisor-panel__empty-icon">🧠</span>
|
<span className="advisor-panel__empty-icon">📋</span>
|
||||||
<p>아직 분석 결과가 없습니다.</p>
|
<p>포트폴리오 탭에서 보유 종목을 먼저 등록해주세요.</p>
|
||||||
<button className="button primary" onClick={() => loadAdvisorAnalysis()}>
|
</div>
|
||||||
분석 시작
|
)}
|
||||||
|
|
||||||
|
{!portfolioLoading && portfolioHoldings.length > 0 && (
|
||||||
|
<div className="advisor-panel__body">
|
||||||
|
{/* 복사 버튼 */}
|
||||||
|
<div className="advisor-prompt__toolbar">
|
||||||
|
<span className="advisor-prompt__info">
|
||||||
|
종목 {portfolioHoldings.length}개 · 총 자산 {totalAssets != null ? formatNumber(totalAssets) + '원' : '미집계'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className={`button primary small ${advisorCopied ? 'is-copied' : ''}`}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(buildAdvisorPrompt());
|
||||||
|
setAdvisorCopied(true);
|
||||||
|
setTimeout(() => setAdvisorCopied(false), 2500);
|
||||||
|
} catch {
|
||||||
|
alert('클립보드 복사에 실패했습니다. 텍스트를 직접 선택해 복사하세요.');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{advisorCopied ? '✅ 복사됨' : '📋 프롬프트 복사'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 프롬프트 미리보기 */}
|
||||||
|
<pre className="advisor-prompt__preview">{buildAdvisorPrompt()}</pre>
|
||||||
|
|
||||||
|
<p className="advisor-panel__disclaimer">
|
||||||
|
※ 이 프롬프트를 AI에 붙여넣으면 전문가 관점의 매매 조언을 받을 수 있습니다.
|
||||||
|
투자 결정은 최종적으로 본인의 판단과 책임 하에 이루어져야 합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user