From 5a1ecfe36d244db08c3aae3d5a06b56ce0ab128b Mon Sep 17 00:00:00 2001 From: kim Date: Fri, 26 Jun 2026 15:48:44 +0900 Subject: [PATCH] =?UTF-8?q?v3.2.1:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EB=A7=A4=ED=95=91=20+=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=A0=84=EC=86=A1=20+=20=EB=82=A0?= =?UTF-8?q?=EC=94=A8=EC=95=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OAuth 로그인 시 카카오 userId 자동 조회 → 현재 계정 자동 매핑 - 채널 링크 나에게 보내기 버튼 (OAuth 완료 후 표시) - 카카오 채널 설정: 링크·등록방법 안내, 기본 탭으로 변경 - 날씨앱: 지역 선택 드롭다운, 🚨 기상특보 버튼, 오버레이 한 줄 배치, 폰트 굵기 개선 - 모바일 브레이크포인트 1024px 통일 (갤럭시 폴드 대응) - 교통앱 마지막 지역 localStorage 저장/복원 Co-Authored-By: Claude Sonnet 4.6 --- src/gateway/server-v2.ts | 44 +++++++++++++++++++++++++++++++++++++++- web-ui/app.js | 19 +++++++++++++++++ web-ui/index.html | 13 +++++++++++- web-ui/weather-app.html | 37 +++++++++++++++++++++++++++------ 4 files changed, 105 insertions(+), 8 deletions(-) diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts index 0c861b6..3583b2e 100644 --- a/src/gateway/server-v2.ts +++ b/src/gateway/server-v2.ts @@ -11603,13 +11603,15 @@ app.get('/api/kakao/auth/start', (req, res) => { const conf = resolveChannelsConfig().kakao; if (!conf.appKey) { res.status(400).send('kakao appKey가 설정되지 않았습니다. vault channels.kakao.appKey 를 먼저 설정하세요.'); return; } const redirectUri = `${req.protocol}://${req.get('host')}/api/kakao/auth/callback`; - const url = `https://kauth.kakao.com/oauth/authorize?client_id=${encodeURIComponent(conf.appKey)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=talk_message`; + const username = (req as any).user?.username || ''; + const url = `https://kauth.kakao.com/oauth/authorize?client_id=${encodeURIComponent(conf.appKey)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=talk_message&state=${encodeURIComponent(username)}`; res.redirect(url); }); // OAuth 콜백 — 카카오가 인가코드를 이 URL로 전송 app.get('/api/kakao/auth/callback', async (req, res) => { const code = String(req.query.code || ''); + const stateUsername = String(req.query.state || ''); if (!code) { res.status(400).send('인가코드가 없습니다.'); return; } const conf = resolveChannelsConfig().kakao; const redirectUri = `${req.protocol}://${req.get('host')}/api/kakao/auth/callback`; @@ -11630,12 +11632,52 @@ app.get('/api/kakao/auth/callback', async (req, res) => { getVault(CONFIG_DIR_PATH).set('channels.kakao.refreshToken', data.refresh_token, 'kakao:token'); kakaoNotify.updateTokens(data.refresh_token, data.access_token); console.log('[KakaoNotify] OAuth 완료 — 리프레시 토큰 vault 저장됨.'); + + // 카카오 userId 조회 → 현재 계정 자동 매핑 + if (stateUsername) { + try { + const meResp = await fetch('https://kapi.kakao.com/v2/user/me', { + headers: { Authorization: `Bearer ${data.access_token}` }, + }); + if (meResp.ok) { + const me = await meResp.json() as { id: number }; + const kakaoId = String(me.id); + const vault = getVault(CONFIG_DIR_PATH); + const rawMap = vault.get('channels.kakao.userMap', 'kakao:usermap')?.expose(); + const userMap: Record = rawMap ? JSON.parse(rawMap) : {}; + if (!userMap[kakaoId]) { + userMap[kakaoId] = stateUsername; + vault.set('channels.kakao.userMap', JSON.stringify(userMap), 'kakao:usermap'); + kakaoChannel.updateConfig({ userMap }); + console.log(`[KakaoNotify] 자동 매핑: kakaoId=${kakaoId} → ${stateUsername}`); + } + } + } catch (e) { console.warn('[KakaoNotify] 자동 매핑 실패:', e); } + } + res.send('

✅ 카카오 연동 완료!

리프레시 토큰이 vault에 저장됐습니다. 이 창을 닫아도 됩니다.

'); } catch (err: any) { res.status(500).send(`OAuth 오류: ${err.message}`); } }); +// 채널 링크 나에게 보내기 +app.post('/api/kakao/send-channel-link', async (req, res) => { + try { + const conf = resolveChannelsConfig().kakao; + if (!conf.appKey || !conf.refreshToken) { + res.json({ success: false, error: 'OAuth 토큰이 없습니다. 카카오 로그인을 먼저 완료하세요.' }); + return; + } + const channelUrl = (req.body?.channelUrl as string) || ''; + const notifier = new KakaoNotify({ appKey: conf.appKey, refreshToken: conf.refreshToken }); + await notifier.send(`📲 SmallClaw 카카오톡 채널\n아래 링크로 채널을 추가하고 AI와 대화하세요!\n\n${channelUrl}`); + res.json({ success: true }); + } catch (err: any) { + res.json({ success: false, error: err.message }); + } +}); + // 알림 테스트 — vault에서 최신 토큰으로 fresh 인스턴스 사용 app.post('/api/kakao/test', async (req, res) => { try { diff --git a/web-ui/app.js b/web-ui/app.js index 5be4eaf..4c368e3 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -3906,6 +3906,8 @@ async function loadKakaoStatus() { else setChannelStatus('kk', 'off', '미설정'); if (n.hasAppKey) document.getElementById('settings-kk-appkey').placeholder = '•••••••• (저장됨)'; document.getElementById('settings-kk-channel-enabled').checked = !!c.enabled; + const linkBtn = document.getElementById('kk-send-link-btn'); + if (linkBtn) linkBtn.style.display = (n.hasRefreshToken) ? 'inline-block' : 'none'; } catch {} } @@ -3928,6 +3930,23 @@ async function kakaoOAuthStart() { }, 1000); } +async function kakaoSendChannelLink() { + const btn = document.getElementById('kk-send-link-btn'); + const orig = btn?.textContent; + if (btn) btn.textContent = '전송 중...'; + try { + const data = await api('/api/kakao/send-channel-link', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channelUrl: 'http://pf.kakao.com/_KxjxexfX/chat' }), + }); + if (btn) btn.textContent = data.success ? '✅ 전송됨!' : '❌ 실패'; + } catch { + if (btn) btn.textContent = '❌ 실패'; + } + setTimeout(() => { if (btn) btn.textContent = orig; }, 3000); +} + async function saveChannelSettings(channel) { const payload = readChannelPayload(channel); try { diff --git a/web-ui/index.html b/web-ui/index.html index 70a53bd..b103d6a 100644 --- a/web-ui/index.html +++ b/web-ui/index.html @@ -1346,8 +1346,8 @@
채널 연결
챗봇 채널 활성화 (오픈빌더 연동) +
+
📲 채널 접속 링크
+ pf.kakao.com/_KxjxexfX/chat +
등록 방법
+
+ 1. 카카오 로그인 → 본인 계정 자동 매핑됨
+ 2. 다른 유저: 위 링크로 채널 추가 후 메시지 전송
+ 3. 서버 로그에서 userId 확인 → 사용자 매핑에 추가 +
+