feat: 설정 자격증명 탭에 키 발급 사이트 카드 추가
Vault에 저장된 키는 이름만 opaque 문자열로 남아 있고 어디서 발급받았는지가 UI 어디에도 없어, 갱신·재발급 때마다 해당 콘솔을 찾아 헤매야 했다. data.go.kr은 특히 하나의 키라도 서비스별로 활용신청이 따로 필요한데(에어코리아 측정소정보가 같은 키로 403 난 이유), 그 사실이 기록된 곳이 없었다. - 서비스 19곳의 발급 페이지 링크. /api/credentials/status로 실제 저장 여부를 읽어 ✓(저장됨)/·(미저장)로 표시하므로 하드코딩된 목록이 아니다. - 공공데이터포털 항목에 "서비스별로 활용신청 필요"를 명시했다. - 전 링크 https + target=_blank + rel=noopener noreferrer, 값은 escHtml 처리. - Vault 조회에 실패해도 링크는 그대로 보이고 경고만 덧붙인다. 저장 여부를 잘못 표시하는 것보다 모른다고 하는 편이 낫다. 브라우저에서 렌더 확인: 19개 중 17개 ✓, 미저장으로 표시된 Brave·OpenAI는 실제 Vault 내용과 일치. 콘솔 에러 없음. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -958,6 +958,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Where each key comes from -->
|
||||
<div style="border:1px solid var(--line);border-radius:12px;padding:14px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||||
<div class="right-section-title">키 발급 사이트</div>
|
||||
<button onclick="loadCredIssuers()" style="font-size:10px;padding:3px 8px;border:1px solid var(--line);border-radius:6px;background:var(--panel-2);cursor:pointer;color:var(--muted)">새로고침</button>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-bottom:10px">✓ 표시는 현재 Vault에 저장돼 있다는 뜻입니다. 갱신·재발급이 필요할 때 여기서 바로 이동하세요.</div>
|
||||
<div id="cred-issuers" style="font-size:12px;color:var(--muted);max-height:320px;overflow-y:auto">Loading…</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit log preview -->
|
||||
<div style="border:1px solid var(--line);border-radius:12px;padding:14px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
||||
|
||||
@@ -192,7 +192,7 @@ function setSettingsTab(tab) {
|
||||
// --- Credentials Tab ---------------------------------------------------------
|
||||
|
||||
async function loadCredentialsTab() {
|
||||
await Promise.all([loadCredFields(), loadCredVaultStatus(), loadCredVaultLog()]);
|
||||
await Promise.all([loadCredFields(), loadCredVaultStatus(), loadCredVaultLog(), loadCredIssuers()]);
|
||||
}
|
||||
|
||||
async function loadCredFields() {
|
||||
@@ -258,6 +258,58 @@ async function loadCredVaultStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// Where each stored credential is issued/renewed. Keys are opaque strings in the vault and the
|
||||
// issuing site is nowhere in the UI, so renewing one meant hunting for the right console — and
|
||||
// data.go.kr in particular needs a *per-service* 활용신청, which is easy to forget (the AirKorea
|
||||
// 측정소정보 service 403'd for exactly that reason on 2026-08-09).
|
||||
const CRED_ISSUERS = [
|
||||
{ key: 'search.tavily_api_key', name: 'Tavily', url: 'https://app.tavily.com/home', note: '웹검색' },
|
||||
{ key: 'search.google_api_key', name: 'Google API', url: 'https://console.cloud.google.com/apis/credentials', note: '검색(CSE)' },
|
||||
{ key: 'search.brave_api_key', name: 'Brave Search', url: 'https://api-dashboard.search.brave.com/app/keys', note: '웹검색' },
|
||||
{ key: 'search.ollama_api_key', name: 'Ollama', url: 'https://ollama.com/settings/keys', note: '클라우드 모델·웹검색' },
|
||||
{ key: 'llm.google.api_key', name: 'Google AI Studio', url: 'https://aistudio.google.com/apikey', note: 'Gemini' },
|
||||
{ key: 'anthropic.api_key', name: 'Anthropic', url: 'https://console.anthropic.com/settings/keys', note: 'Claude' },
|
||||
{ key: 'llm.openai.api_key', name: 'OpenAI', url: 'https://platform.openai.com/api-keys', note: 'GPT' },
|
||||
{ key: 'openweather.api_key', name: 'OpenWeather', url: 'https://home.openweathermap.org/api_keys', note: '날씨' },
|
||||
{ key: 'kma.api_key', name: '공공데이터포털', url: 'https://www.data.go.kr/iim/api/selectAPIAcountView.do', note: '기상청·에어코리아 — 서비스별로 활용신청 필요' },
|
||||
{ key: 'cds.api_key', name: 'Copernicus CDS', url: 'https://cds.climate.copernicus.eu/profile', note: '기후 재분석' },
|
||||
{ key: 'news.newsdata_api_key', name: 'NewsData.io', url: 'https://newsdata.io/api-key', note: '뉴스' },
|
||||
{ key: 'pubmed.api_key', name: 'NCBI PubMed', url: 'https://account.ncbi.nlm.nih.gov/settings/', note: '논문' },
|
||||
{ key: 'traffic.its_api_key', name: 'ITS 국가교통정보', url: 'https://www.its.go.kr/opendata/', note: '교통' },
|
||||
{ key: 'ppt.unsplash_key', name: 'Unsplash', url: 'https://unsplash.com/oauth/applications', note: 'PPT 이미지' },
|
||||
{ key: 'ppt.pexels_key', name: 'Pexels', url: 'https://www.pexels.com/api/', note: 'PPT 이미지' },
|
||||
{ key: 'ppt.pixabay_key', name: 'Pixabay', url: 'https://pixabay.com/api/docs/', note: 'PPT 이미지' },
|
||||
{ key: 'channels.telegram.botToken', name: 'Telegram BotFather', url: 'https://t.me/BotFather', note: '텔레그램 봇' },
|
||||
{ key: 'channels.kakao.appKey', name: 'Kakao Developers', url: 'https://developers.kakao.com/console/app', note: '카카오' },
|
||||
{ key: 'writer.providers.aladin.apiKey', name: '알라딘 TTB', url: 'https://www.aladin.co.kr/ttb/wblog_manage.aspx', note: '도서 검색' },
|
||||
];
|
||||
|
||||
async function loadCredIssuers() {
|
||||
const el = document.getElementById('cred-issuers');
|
||||
if (!el) return;
|
||||
let stored = new Set();
|
||||
try {
|
||||
const data = await api('/api/credentials/status');
|
||||
stored = new Set(data.keys || []);
|
||||
} catch (e) {
|
||||
// Listing the sites is still useful without the vault — just don't claim what is stored.
|
||||
el.innerHTML = `<div style="color:var(--warn);font-size:11px;margin-bottom:8px">저장 상태를 못 읽었습니다 (${escHtml(e.message)}) — 링크만 표시합니다.</div>`;
|
||||
}
|
||||
const rows = CRED_ISSUERS.map(s => {
|
||||
const has = stored.has(s.key);
|
||||
const mark = has
|
||||
? '<span style="color:var(--ok);font-size:12px;flex-shrink:0" title="Vault에 저장됨">✓</span>'
|
||||
: '<span style="color:var(--muted);opacity:.45;font-size:12px;flex-shrink:0" title="미저장">·</span>';
|
||||
return `<div style="display:flex;align-items:baseline;gap:7px;margin-bottom:6px;line-height:1.45">
|
||||
${mark}
|
||||
<a href="${escHtml(s.url)}" target="_blank" rel="noopener noreferrer"
|
||||
style="color:var(--accent, #2563eb);text-decoration:none;font-size:12px;flex-shrink:0">${escHtml(s.name)}</a>
|
||||
<span style="font-size:10.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis">${escHtml(s.note)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
el.innerHTML = (el.innerHTML.includes('저장 상태를 못 읽었습니다') ? el.innerHTML : '') + rows;
|
||||
}
|
||||
|
||||
async function loadCredVaultLog() {
|
||||
const el = document.getElementById('cred-vault-log');
|
||||
if (!el) return;
|
||||
|
||||
Reference in New Issue
Block a user