v3.2.1: 카카오 자동매핑 + 채널링크 전송 + 날씨앱 개선

- OAuth 로그인 시 카카오 userId 자동 조회 → 현재 계정 자동 매핑
- 채널 링크 나에게 보내기 버튼 (OAuth 완료 후 표시)
- 카카오 채널 설정: 링크·등록방법 안내, 기본 탭으로 변경
- 날씨앱: 지역 선택 드롭다운, 🚨 기상특보 버튼, 오버레이 한 줄 배치, 폰트 굵기 개선
- 모바일 브레이크포인트 1024px 통일 (갤럭시 폴드 대응)
- 교통앱 마지막 지역 localStorage 저장/복원

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-26 15:48:44 +09:00
co-authored by Claude Sonnet 4.6
parent d74614cee1
commit 5a1ecfe36d
4 changed files with 105 additions and 8 deletions
+19
View File
@@ -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 {
+12 -1
View File
@@ -1346,8 +1346,8 @@
<div class="right-section-title" style="margin-bottom:8px">채널 연결</div>
<label style="display:block;font-size:12px;color:var(--muted);margin-bottom:6px">채널</label>
<select id="settings-channel-select" class="settings-input" onchange="onChannelTypeChange()" style="margin-bottom:10px">
<option value="telegram">Telegram</option>
<option value="kakao">카카오톡</option>
<option value="telegram">Telegram</option>
<option value="discord">Discord</option>
<option value="whatsapp">WhatsApp</option>
<option value="email">Email</option>
@@ -1392,6 +1392,7 @@
<div style="font-size:11px;font-weight:700;color:var(--muted);margin-bottom:6px">OAuth 로그인 (나에게 보내기 알림용)</div>
<div style="font-size:11px;color:var(--muted);margin-bottom:8px">API 키 저장 후 아래 버튼으로 카카오 로그인하면 토큰이 자동 저장됩니다.</div>
<button onclick="kakaoOAuthStart()" style="background:#FEE500;border:none;border-radius:7px;padding:6px 14px;font-size:12px;font-weight:700;color:#3C1E1E;cursor:pointer">🟡 카카오 로그인</button>
<button id="kk-send-link-btn" onclick="kakaoSendChannelLink()" style="display:none;background:none;border:1px solid rgba(56,189,248,.4);border-radius:7px;padding:6px 12px;font-size:12px;font-weight:700;color:#38bdf8;cursor:pointer;margin-left:6px">📲 채널 링크 보내기</button>
<span id="kk-oauth-status" style="font-size:11px;color:var(--muted);margin-left:8px"></span>
</div>
<label style="display:block;font-size:12px;color:var(--muted);margin:10px 0 6px">사용자 매핑 (선택, JSON)</label>
@@ -1400,6 +1401,16 @@
<input type="checkbox" id="settings-kk-channel-enabled" />
챗봇 채널 활성화 (오픈빌더 연동)
</label>
<div style="margin-top:14px;padding:10px 12px;border:1px solid rgba(254,229,0,.25);border-radius:8px;background:rgba(254,229,0,.04);font-size:11px;line-height:2;">
<div style="font-weight:700;color:#b8960a;margin-bottom:4px">📲 채널 접속 링크</div>
<a href="http://pf.kakao.com/_KxjxexfX/chat" target="_blank" style="color:#38bdf8;word-break:break-all;">pf.kakao.com/_KxjxexfX/chat</a>
<div style="margin-top:8px;font-weight:700;color:var(--muted)">등록 방법</div>
<div style="color:var(--muted)">
1. 카카오 로그인 → 본인 계정 자동 매핑됨<br>
2. 다른 유저: 위 링크로 채널 추가 후 메시지 전송<br>
3. 서버 로그에서 userId 확인 → 사용자 매핑에 추가
</div>
</div>
</div>
<div id="channel-form-discord" style="display:none">
+31 -6
View File
@@ -10,7 +10,7 @@
<style>
:root{--brand:#38bdf8;}
html,body{height:100%;margin:0;padding:0;}
body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;display:flex;flex-direction:column;overflow:hidden;height:100dvh;}
body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;font-weight:500;display:flex;flex-direction:column;overflow:hidden;height:100dvh;}
/* ── Header ── */
.wt-hdr{display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--panel);border-bottom:1px solid var(--line);flex-shrink:0;}
@@ -40,7 +40,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
.wt-loc.active{background:rgba(56,189,248,.08);border-color:rgba(56,189,248,.25);}
.wt-loc-info{flex:1;min-width:0;}
.wt-loc-name{font-size:11px;font-weight:700;color:var(--text);display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.wt-loc-sub{font-size:10px;color:var(--muted);display:block;}
.wt-loc-sub{font-size:10px;font-weight:500;color:var(--muted);display:block;}
.wt-loc.active .wt-loc-sub{color:#7dd3fc;}
.wt-loc-acts{display:flex;gap:1px;opacity:0;transition:.12s;flex-shrink:0;}
.wt-loc-act{background:none;border:none;cursor:pointer;font-size:11px;color:var(--muted);padding:2px 3px;border-radius:3px;transition:.12s;line-height:1;}
@@ -60,7 +60,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
/* Quick prompts */
.wt-quick{padding:0 6px 6px;}
.wt-qbtn{background:none;border:1px solid var(--line);border-radius:6px;padding:5px 9px;font-size:11px;color:var(--muted);cursor:pointer;font-family:inherit;text-align:left;transition:.15s;width:100%;margin-bottom:3px;line-height:1.4;}
.wt-qbtn{background:none;border:1px solid var(--line);border-radius:6px;padding:5px 9px;font-size:11px;font-weight:600;color:var(--muted);cursor:pointer;font-family:inherit;text-align:left;transition:.15s;width:100%;margin-bottom:3px;line-height:1.4;}
.wt-qbtn:hover{border-color:#38bdf8;color:#38bdf8;background:rgba(56,189,248,.04);}
/* Alert badges */
@@ -80,7 +80,9 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
.wt-go-btn:hover{background:#0e5a80;}
/* Overlay tabs */
.wt-overlays{display:flex;gap:4px;flex-wrap:wrap;}
.wt-overlays{display:flex;gap:4px;flex-wrap:nowrap;overflow-x:auto;padding-bottom:2px;}
.wt-overlays::-webkit-scrollbar{height:3px;}
.wt-overlays::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
.wt-ov{padding:3px 9px;border:1.5px solid var(--line);border-radius:16px;font-size:11px;font-weight:600;cursor:pointer;background:none;color:var(--muted);font-family:inherit;transition:.12s;white-space:nowrap;}
.wt-ov:hover{border-color:#38bdf8;color:#38bdf8;}
.wt-ov.active{border-color:#0ea5e9;background:#082f49;color:#7dd3fc;}
@@ -202,6 +204,8 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
<div class="wt-divider"></div>
<div class="wt-sb-sec"><span>빠른 질문</span></div>
<div class="wt-quick">
<select id="quick-loc-sel" class="wt-lf-input" style="width:100%;margin-bottom:6px;font-size:11px;" onchange="localStorage.setItem('wt_quick_loc',this.value)"></select>
<button class="wt-qbtn" onclick="askWeather('alert')" style="color:#f87171;border-color:rgba(239,68,68,.3);">🚨 기상 특보</button>
<button class="wt-qbtn" onclick="askWeather('today')">☀️ 오늘 날씨</button>
<button class="wt-qbtn" onclick="askWeather('week')">📅 주간 예보</button>
<button class="wt-qbtn" onclick="askWeather('air')">🌫️ 미세먼지·대기질</button>
@@ -250,7 +254,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
</div>
<div class="wt-map-area">
<div class="wt-map-loading" id="map-loading">🌍 Windy 지도 로딩 중...</div>
<iframe class="wt-map-frame" id="windy-frame" src="about:blank" onload="document.getElementById('map-loading').classList.add('hidden')"></iframe>
<iframe class="wt-map-frame" id="windy-frame" src="about:blank" allow="geolocation" onload="document.getElementById('map-loading').classList.add('hidden')"></iframe>
</div>
</section>
@@ -369,6 +373,7 @@ function escHtml(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt
function escAttr(s){return String(s||'').replace(/'/g,'&#39;').replace(/"/g,'&quot;');}
function renderLocList(){
updateQuickLocSel();
const locs=getLocs();
const container=document.getElementById('loc-list');
container.innerHTML=`<div class="wt-locs">${locs.map((l,i)=>`
@@ -492,7 +497,21 @@ function processWindyUrl(text){
}
// Quick ask helpers
const currentLoc=()=>{const locs=getLocs();return locs[0]?.name||'서울';};
function updateQuickLocSel(){
const sel=document.getElementById('quick-loc-sel');
if(!sel)return;
const locs=getLocs();
const saved=localStorage.getItem('wt_quick_loc');
const presets=['서울','부산','대구','인천','광주','대전','울산','수원','구미','강릉','제주'];
const locNames=locs.map(l=>l.name);
const all=[...new Set([...locNames,...presets])];
sel.innerHTML=all.map(n=>`<option value="${n}"${n===(saved||locNames[0]||'서울')?'selected':''}>${n}</option>`).join('');
}
const currentLoc=()=>{
const sel=document.getElementById('quick-loc-sel');
if(sel&&sel.value)return sel.value;
const locs=getLocs();return locs[0]?.name||'서울';
};
function askWeather(type){
const loc=currentLoc();
const msgs={
@@ -504,10 +523,16 @@ function askWeather(type){
uv:`${loc} 오늘 자외선 지수와 시간대별 변화를 알려주세요.`,
climate:`${loc}의 월별 기후 평균(기온·강수량)을 30년 기준으로 알려주세요.`,
past:`${loc} 오늘 날짜 기준 역대 같은 날 날씨 기록을 알려주세요.`,
alert:`현재 ${loc} 및 전국 기상 특보(태풍·호우·폭염·한파·강풍·풍랑·황사)가 발효 중인지 알려주세요. 특보 종류, 대상 지역, 해제 예상 시간을 포함해주세요.`,
};
sendToChat(msgs[type]||'날씨를 알려주세요.');
}
function fetchAlerts(){
document.getElementById('alert-content').textContent='💬 채팅창에서 확인하세요';
sendToChat('현재 한국 전국 기상 특보(태풍·호우·폭염·한파·강풍·황사 등)가 발효 중인지 알려주세요. 없으면 "현재 특보 없음"이라고만 답하세요.');
}
// ── Chat ──────────────────────────────────────────────────────────────────────
function addMsg(role,html){
const msgs=document.getElementById('chat-msgs');