v3.2.0: 카카오톡 연동 + 탐정앱 업그레이드 + 모바일 대응 강화

- 카카오톡 채널 챗봇 (오픈빌더 비동기 콜백 방식, 5초 제한 우회)
- 카카오 "나에게 보내기" OAuth 알림 (kakao-notify.ts)
- 채널 설정 UI + userMap으로 다중 사용자 계정 매핑
- 탐정앱: 탭(메모/체크리스트/타임라인), 우선순위, 검색, 보고서/고소장/접수안내 생성
- 모바일 브레이크포인트 640px → 1024px (갤럭시 폴드 대응)
- 프롬프트 파일 제한 상향 (USER.md 3000, SOUL.md/AGENTS.md 4000)
- 교통앱 마지막 선택 지역 localStorage 저장/복원
- docs/kakao-setup.md 작성

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-26 11:15:27 +09:00
co-authored by Claude Sonnet 4.6
parent cea916be78
commit d74614cee1
23 changed files with 3761 additions and 122 deletions
+39 -7
View File
@@ -156,7 +156,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
.mn-resize:hover,.mn-resize.dragging{background:#8b5cf6;}
/* ── Mobile ── */
@media(max-width:640px){
@media(max-width:1024px){
.mn-hdr{flex-wrap:wrap;padding:5px 8px;gap:5px;}
.mn-hdr h1{font-size:12px;}
.mode-toggle{order:10;flex:0 0 100%;}
@@ -198,7 +198,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
/* font-size ≥16px to prevent iOS auto-zoom */
.mn-textarea,input,textarea{font-size:16px !important;}
}
@media(min-width:641px){
@media(min-width:1025px){
#mob-tabbar{display:none !important;}
#mob-backdrop{display:none !important;}
}
@@ -375,7 +375,7 @@ function getToken(){try{return sessionStorage.getItem(TOKEN_KEY)||localStorage.g
function authH(extra){const t=getToken();const h={...(extra||{})};if(t)h['Authorization']='Bearer '+t;return h;}
async function checkAuth(){
if(!getToken()){location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}
try{const r=await fetch('/api/auth/status',{headers:authH()});if(!r.ok){location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}return true;}
try{const r=await fetch('/api/auth/status',{headers:authH()});if(!r.ok){location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}const d=await r.json();if(!d.authenticated){location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}return true;}
catch{location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}
}
@@ -472,8 +472,37 @@ async function loadModels(){
function saveModel(){selectedModel=document.getElementById('model-sel').value;try{localStorage.setItem(MODEL_KEY,selectedModel);}catch{}}
// ── Session ───────────────────────────────────────────────────────────────────
const SID_KEY='mn_app_session';
function getSessionId(){try{let s=sessionStorage.getItem(SID_KEY);if(!s){s='mn_'+Math.random().toString(36).slice(2,12);sessionStorage.setItem(SID_KEY,s);}return s;}catch{return 'mn_main';}}
const APP_KEY='mn';
let _sessionId=null;
function getSessionId(){return _sessionId||'mn_main';}
async function initSessionId(){
try{const r=await fetch('/api/user/app-sessions',{headers:authH()});if(r.ok){const d=await r.json();if(d[APP_KEY]){_sessionId=d[APP_KEY];return;}}}catch{}
_sessionId='mn_'+Math.random().toString(36).slice(2,12);
try{await fetch('/api/user/app-sessions',{method:'PUT',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({appKey:APP_KEY,sessionId:_sessionId})});}catch{}
}
async function loadHistory(){
const sid=getSessionId();
if(!sid||sid.endsWith('_main'))return;
try{
const r=await fetch('/api/chat/sessions/'+encodeURIComponent(sid),{headers:authH()});
if(!r.ok)return;
const d=await r.json();
const msgs=(d.history||[]).filter(m=>m.role==='user'||m.role==='assistant');
if(!msgs.length)return;
const recent=msgs.slice(-30);
const el=document.getElementById('chat-msgs');
const sep=document.createElement('div');sep.style.cssText='text-align:center;color:var(--muted,#888);font-size:11px;padding:6px 0;';sep.textContent='─ 이전 대화 ─';el.appendChild(sep);
for(const m of recent){
if(m.role==='user')addMsg('user',escHtml(m.content||''));
else addMsg('assistant',renderMd(m.content||''));
}
const sep2=document.createElement('div');sep2.style.cssText='text-align:center;color:var(--muted,#888);font-size:11px;padding:6px 0;';sep2.textContent='─ 이어서 ─';el.appendChild(sep2);
el.scrollTop=el.scrollHeight;
}catch{}
}
// ── Tools: show / hide ────────────────────────────────────────────────────────
let activeTool=null;
@@ -1067,10 +1096,11 @@ async function callChat(message){
}finally{btn.disabled=false;btn.textContent='전송';}
}
function clearChat(){
async function clearChat(){
if(!confirm('대화를 지울까요?'))return;
document.getElementById('chat-msgs').innerHTML='';
try{sessionStorage.removeItem(SID_KEY);}catch{}
_sessionId='mn_'+Math.random().toString(36).slice(2,12);
try{await fetch('/api/user/app-sessions',{method:'PUT',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({appKey:APP_KEY,sessionId:_sessionId})});}catch{}
addMsg('system','새 대화가 시작되었습니다. 편하게 이야기해주세요.');
}
function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}}
@@ -1079,6 +1109,8 @@ function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrol
// ── Init ──────────────────────────────────────────────────────────────────────
(async()=>{
const ok=await checkAuth();if(!ok)return;
await initSessionId();
await loadHistory();
setMode(currentMode);
await Promise.all([loadSkills(),loadModels()]);
addMsg('system','🧠 마음 상담실에 오신 걸 환영합니다. 왼쪽 도구를 활용하거나 편하게 이야기해주세요.');