AI Coach: 클라이언트 API 키 제거, 백엔드 프록시로 전환
- Anthropic API 직접 호출 → /api/stock/ai-coach 백엔드 프록시로 변경 - API 키 입력 UI 제거 (서버에서 관리) - aiApiKey 상태 변수 및 localStorage 저장 로직 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -232,8 +232,7 @@ const StockTrade = () => {
|
|||||||
const [reportSortDir, setReportSortDir] = useState('desc');
|
const [reportSortDir, setReportSortDir] = useState('desc');
|
||||||
|
|
||||||
/* AI Coach */
|
/* AI Coach */
|
||||||
const [aiApiKey, setAiApiKey] = useState('');
|
const [aiModel, setAiModel] = useState(() => localStorage.getItem('ai_coach_model') ?? 'claude-haiku-4-5-20251001');
|
||||||
const [aiModel, setAiModel] = useState('claude-haiku-4-5-20251001');
|
|
||||||
const [aiResult, setAiResult] = useState(null);
|
const [aiResult, setAiResult] = useState(null);
|
||||||
const [aiLoading, setAiLoading] = useState(false);
|
const [aiLoading, setAiLoading] = useState(false);
|
||||||
const [aiError, setAiError] = useState('');
|
const [aiError, setAiError] = useState('');
|
||||||
@@ -377,12 +376,8 @@ const StockTrade = () => {
|
|||||||
}
|
}
|
||||||
}, [activeTab, assetHistoryDays, loadAssetHistory]);
|
}, [activeTab, assetHistoryDays, loadAssetHistory]);
|
||||||
|
|
||||||
/* AI Coach: 마운트 시 localStorage에서 API Key + 오늘 캐시 복원 */
|
/* AI Coach: 마운트 시 오늘 캐시 복원 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedKey = localStorage.getItem('ai_coach_key') ?? '';
|
|
||||||
const savedModel = localStorage.getItem('ai_coach_model') ?? 'claude-haiku-4-5-20251001';
|
|
||||||
setAiApiKey(savedKey);
|
|
||||||
setAiModel(savedModel);
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const cached = localStorage.getItem(`ai_coach_${today}`);
|
const cached = localStorage.getItem(`ai_coach_${today}`);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
@@ -707,7 +702,7 @@ const StockTrade = () => {
|
|||||||
/* ── AI coach ────────────────────────────────────────────────── */
|
/* ── AI coach ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
const handleAiCoach = async () => {
|
const handleAiCoach = async () => {
|
||||||
if (!aiApiKey.trim() || portfolioHoldings.length === 0) return;
|
if (portfolioHoldings.length === 0) return;
|
||||||
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const cacheKey = `ai_coach_${today}`;
|
const cacheKey = `ai_coach_${today}`;
|
||||||
@@ -755,24 +750,15 @@ ${holdingsText}${marketText}
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
const res = await fetch('/api/stock/ai-coach', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ model: aiModel, prompt, max_tokens: 1024 }),
|
||||||
'x-api-key': aiApiKey.trim(),
|
|
||||||
'anthropic-version': '2023-06-01',
|
|
||||||
'anthropic-dangerous-direct-browser-access': 'true',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: aiModel,
|
|
||||||
max_tokens: 1024,
|
|
||||||
messages: [{ role: 'user', content: prompt }],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => '');
|
const errData = await res.json().catch(() => ({}));
|
||||||
throw new Error(`Claude API 오류 (${res.status}): ${text.slice(0, 200)}`);
|
throw new Error(errData.error || `AI Coach 오류 (${res.status})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -2467,30 +2453,8 @@ ${cashLines}
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* API Key 설정 */}
|
{/* 모델 선택 */}
|
||||||
<div className="ai-coach-settings">
|
<div className="ai-coach-settings">
|
||||||
<label>
|
|
||||||
Anthropic API Key
|
|
||||||
<div className="ai-coach-key-row">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="ai-coach-key-input"
|
|
||||||
value={aiApiKey}
|
|
||||||
onChange={(e) => setAiApiKey(e.target.value)}
|
|
||||||
placeholder="sk-ant-api03-..."
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="button ghost small"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
localStorage.setItem('ai_coach_key', aiApiKey);
|
|
||||||
localStorage.setItem('ai_coach_model', aiModel);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
저장
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label>
|
<label>
|
||||||
AI 모델
|
AI 모델
|
||||||
<select
|
<select
|
||||||
@@ -2511,7 +2475,7 @@ ${cashLines}
|
|||||||
className="button primary"
|
className="button primary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAiCoach}
|
onClick={handleAiCoach}
|
||||||
disabled={aiLoading || !aiApiKey.trim() || portfolioHoldings.length === 0}
|
disabled={aiLoading || portfolioHoldings.length === 0}
|
||||||
>
|
>
|
||||||
{aiLoading ? 'AI 분석 중...' : '오늘 투자 평가 받기'}
|
{aiLoading ? 'AI 분석 중...' : '오늘 투자 평가 받기'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user