feat: AI CRM 어드민 대시보드 + 바텀시트 드래그 닫기 + UTF-8 인코딩 + 문서화

- /admin/ai-crm: AI 업셀링 추천 생성 현황 대시보드 (통계 카드 + 로그 테이블 + 아코디언 상세)
- 마이페이지 바텀시트: 터치 드래그로 닫기 기능 추가 (80px 임계값)
- Windows 콘솔 UTF-8 인코딩 강제 (app.py, clawdbot_client.py)
- admin.html 헤더에 AI CRM 네비 링크 추가
- docs: ai-upselling-crm.md, windows-utf8-encoding.md 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin
2026-02-26 20:38:04 +09:00
parent b5a99f7b3b
commit 5042cffb9f
7 changed files with 840 additions and 15 deletions

View File

@@ -396,16 +396,21 @@
<!-- AI 추천 바텀시트 -->
<div id="rec-sheet" style="display:none;">
<div id="rec-backdrop" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:999;animation:recFadeIn .3s ease;"></div>
<div id="rec-content" style="position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:420px;background:#fff;border-radius:24px 24px 0 0;padding:12px 24px 32px;box-shadow:0 -8px 32px rgba(0,0,0,0.12);z-index:1000;animation:recSlideUp .4s cubic-bezier(.16,1,.3,1);">
<div style="width:40px;height:4px;background:#dee2e6;border-radius:2px;margin:0 auto 20px;"></div>
<div style="text-align:center;padding:8px 0 20px;">
<div style="font-size:48px;margin-bottom:16px;">💊</div>
<div id="rec-message" style="color:#343a40;font-size:16px;font-weight:500;line-height:1.6;letter-spacing:-0.3px;margin-bottom:16px;"></div>
<div id="rec-product" style="display:inline-block;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:14px;font-weight:600;padding:8px 20px;border-radius:20px;letter-spacing:-0.2px;"></div>
<div id="rec-content" style="position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:420px;background:#fff;border-radius:24px 24px 0 0;padding:0 0 0;box-shadow:0 -8px 32px rgba(0,0,0,0.12);z-index:1000;animation:recSlideUp .4s cubic-bezier(.16,1,.3,1);touch-action:none;">
<!-- 드래그 핸들 영역 -->
<div id="rec-drag-handle" style="padding:12px 24px 0;cursor:grab;">
<div style="width:40px;height:4px;background:#dee2e6;border-radius:2px;margin:0 auto 20px;"></div>
</div>
<div style="display:flex;gap:12px;padding-bottom:env(safe-area-inset-bottom,0);">
<button onclick="dismissRec()" style="flex:1;padding:14px;border:1px solid #dee2e6;border-radius:14px;background:#fff;color:#868e96;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;">다음에요</button>
<button onclick="dismissRec()" style="flex:2;padding:14px;border:none;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;">관심있어요!</button>
<div style="padding:0 24px 32px;">
<div style="text-align:center;padding:8px 0 20px;">
<div style="font-size:48px;margin-bottom:16px;">💊</div>
<div id="rec-message" style="color:#343a40;font-size:16px;font-weight:500;line-height:1.6;letter-spacing:-0.3px;margin-bottom:16px;"></div>
<div id="rec-product" style="display:inline-block;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:14px;font-weight:600;padding:8px 20px;border-radius:20px;letter-spacing:-0.2px;"></div>
</div>
<div style="display:flex;gap:12px;padding-bottom:env(safe-area-inset-bottom,0);">
<button onclick="dismissRec()" style="flex:1;padding:14px;border:1px solid #dee2e6;border-radius:14px;background:#fff;color:#868e96;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;">다음에요</button>
<button onclick="dismissRec()" style="flex:2;padding:14px;border:none;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;">관심있어요!</button>
</div>
</div>
</div>
</div>
@@ -416,6 +421,87 @@
</style>
<script>
let _recId = null;
// ── 드래그 닫기 ──
(function() {
let startY = 0, currentY = 0, isDragging = false;
const DISMISS_THRESHOLD = 80;
function getContent() { return document.getElementById('rec-content'); }
function getBackdrop() { return document.getElementById('rec-backdrop'); }
function onStart(y) {
const c = getContent();
if (!c) return;
isDragging = true;
startY = y;
currentY = 0;
c.style.animation = 'none';
c.style.transition = 'none';
}
function onMove(y) {
if (!isDragging) return;
const c = getContent();
const b = getBackdrop();
currentY = Math.max(0, y - startY); // 아래로만
c.style.transform = 'translate(-50%, ' + currentY + 'px)';
// 배경 투명도도 같이
const opacity = Math.max(0, 0.3 * (1 - currentY / 300));
b.style.background = 'rgba(0,0,0,' + opacity + ')';
}
function onEnd() {
if (!isDragging) return;
isDragging = false;
const c = getContent();
if (currentY > DISMISS_THRESHOLD) {
// 충분히 내렸으면 닫기
c.style.transition = 'transform .25s ease';
c.style.transform = 'translate(-50%, 100%)';
getBackdrop().style.transition = 'opacity .25s';
getBackdrop().style.opacity = '0';
setTimeout(function() {
document.getElementById('rec-sheet').style.display = 'none';
c.style.transition = '';
c.style.transform = '';
}, 250);
if (_recId) fetch('/api/recommendation/' + _recId + '/dismiss', {method:'POST'}).catch(function(){});
} else {
// 복귀
c.style.transition = 'transform .25s cubic-bezier(.16,1,.3,1)';
c.style.transform = 'translate(-50%, 0)';
getBackdrop().style.transition = 'background .25s';
getBackdrop().style.background = 'rgba(0,0,0,0.3)';
setTimeout(function() { c.style.transition = ''; }, 250);
}
}
document.addEventListener('DOMContentLoaded', function() {
var el = document.getElementById('rec-content');
if (!el) return;
// 터치 (모바일)
el.addEventListener('touchstart', function(e) {
onStart(e.touches[0].clientY);
}, {passive: true});
el.addEventListener('touchmove', function(e) {
if (isDragging && currentY > 0) e.preventDefault();
onMove(e.touches[0].clientY);
}, {passive: false});
el.addEventListener('touchend', onEnd);
// 마우스 (데스크톱 테스트용)
el.addEventListener('mousedown', function(e) {
if (e.target.tagName === 'BUTTON') return;
onStart(e.clientY);
});
document.addEventListener('mousemove', function(e) {
if (isDragging) onMove(e.clientY);
});
document.addEventListener('mouseup', onEnd);
});
})();
// ── 추천 로드 ──
window.addEventListener('load', function() {
{% if user_id %}
setTimeout(async function() {
@@ -429,17 +515,25 @@
document.getElementById('rec-sheet').style.display = 'block';
document.getElementById('rec-backdrop').onclick = dismissRec;
}
} catch(e) {}
} catch(e) {
console.error('[AI추천] 에러:', e);
}
}, 1500);
{% endif %}
});
function dismissRec() {
const c = document.getElementById('rec-content');
const b = document.getElementById('rec-backdrop');
c.style.animation = 'recSlideDown .3s ease forwards';
c.style.transition = 'transform .3s ease';
c.style.transform = 'translate(-50%, 100%)';
b.style.opacity = '0';
b.style.transition = 'opacity .3s';
setTimeout(function(){ document.getElementById('rec-sheet').style.display='none'; }, 300);
setTimeout(function(){
document.getElementById('rec-sheet').style.display='none';
c.style.transition = '';
c.style.transform = '';
}, 300);
if (_recId) fetch('/api/recommendation/' + _recId + '/dismiss', {method:'POST'}).catch(function(){});
}
</script>