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:
2026-04-03 01:12:37 +09:00
parent 8fcfb6b000
commit 314702cb66

View File

@@ -232,8 +232,7 @@ const StockTrade = () => {
const [reportSortDir, setReportSortDir] = useState('desc');
/* AI Coach */
const [aiApiKey, setAiApiKey] = useState('');
const [aiModel, setAiModel] = useState('claude-haiku-4-5-20251001');
const [aiModel, setAiModel] = useState(() => localStorage.getItem('ai_coach_model') ?? 'claude-haiku-4-5-20251001');
const [aiResult, setAiResult] = useState(null);
const [aiLoading, setAiLoading] = useState(false);
const [aiError, setAiError] = useState('');
@@ -377,12 +376,8 @@ const StockTrade = () => {
}
}, [activeTab, assetHistoryDays, loadAssetHistory]);
/* AI Coach: 마운트 시 localStorage에서 API Key + 오늘 캐시 복원 */
/* AI Coach: 마운트 시 오늘 캐시 복원 */
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 cached = localStorage.getItem(`ai_coach_${today}`);
if (cached) {
@@ -707,7 +702,7 @@ const StockTrade = () => {
/* ── AI coach ────────────────────────────────────────────────── */
const handleAiCoach = async () => {
if (!aiApiKey.trim() || portfolioHoldings.length === 0) return;
if (portfolioHoldings.length === 0) return;
const today = new Date().toISOString().slice(0, 10);
const cacheKey = `ai_coach_${today}`;
@@ -755,24 +750,15 @@ ${holdingsText}${marketText}
}`;
try {
const res = await fetch('https://api.anthropic.com/v1/messages', {
const res = await fetch('/api/stock/ai-coach', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'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 }],
}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: aiModel, prompt, max_tokens: 1024 }),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`Claude API 오류 (${res.status}): ${text.slice(0, 200)}`);
const errData = await res.json().catch(() => ({}));
throw new Error(errData.error || `AI Coach 오류 (${res.status})`);
}
const data = await res.json();
@@ -2467,30 +2453,8 @@ ${cashLines}
</div>
)}
{/* API Key 설정 */}
{/* 모델 선택 */}
<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>
AI 모델
<select
@@ -2511,7 +2475,7 @@ ${cashLines}
className="button primary"
type="button"
onClick={handleAiCoach}
disabled={aiLoading || !aiApiKey.trim() || portfolioHoldings.length === 0}
disabled={aiLoading || portfolioHoldings.length === 0}
>
{aiLoading ? 'AI 분석 중...' : '오늘 투자 평가 받기'}
</button>