From ce684a6cf3ceae95d64631ccd8c9d67661980fb2 Mon Sep 17 00:00:00 2001 From: kim Date: Sun, 9 Aug 2026 10:40:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=EC=9E=90=EA=B2=A9?= =?UTF-8?q?=EC=A6=9D=EB=AA=85=20=ED=83=AD=EC=97=90=20=ED=82=A4=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web-ui/html/index.html | 10 ++++++ web-ui/js/app/settings-provider.js | 54 +++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/web-ui/html/index.html b/web-ui/html/index.html index ef0050e..04b40db 100644 --- a/web-ui/html/index.html +++ b/web-ui/html/index.html @@ -958,6 +958,16 @@ + +
+
+
키 발급 사이트
+ +
+
✓ 표시는 현재 Vault에 저장돼 있다는 뜻입니다. 갱신·재발급이 필요할 때 여기서 바로 이동하세요.
+
Loading…
+
+
diff --git a/web-ui/js/app/settings-provider.js b/web-ui/js/app/settings-provider.js index f240ec7..031c98e 100644 --- a/web-ui/js/app/settings-provider.js +++ b/web-ui/js/app/settings-provider.js @@ -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 = `
저장 상태를 못 읽었습니다 (${escHtml(e.message)}) — 링크만 표시합니다.
`; + } + const rows = CRED_ISSUERS.map(s => { + const has = stored.has(s.key); + const mark = has + ? '✓' + : '·'; + return `
+ ${mark} + ${escHtml(s.name)} + ${escHtml(s.note)} +
`; + }).join(''); + el.innerHTML = (el.innerHTML.includes('저장 상태를 못 읽었습니다') ? el.innerHTML : '') + rows; +} + async function loadCredVaultLog() { const el = document.getElementById('cred-vault-log'); if (!el) return;