v3.1.0: 모바일 대응 + 미디 마스터채널 + Content-Disposition 수정

- music-app: 마스터채널 추가 (볼륨 0–200%, FX 항상 표시), 효과 토글 패널 제거
- routes-music.ts: 한글 파일명 Content-Disposition 500 에러 수정 (Buffer.ascii → JS replace)
- 모바일 오버레이 패턴 (max-width:640px): mind/accountant/investor/lawyer/language/pptx-wizard/dental-agent
  각 앱 사이드바·채팅 패널을 fixed 오버레이로, 바텀 탭바로 네비게이션

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-23 13:43:46 +09:00
co-authored by Claude Sonnet 4.6
parent c637448f25
commit cea916be78
9 changed files with 1479 additions and 266 deletions
+1 -1
View File
@@ -248,7 +248,7 @@ app.get('/api/music/workspace-file/:dir/:file', (req, res) => {
res.status(403).json({ error: 'Forbidden' }); return;
}
if (!fs.existsSync(filePath)) { res.status(404).json({ error: 'Not found' }); return; }
const safeName = Buffer.from(req.params.file).toString('ascii').replace(/[^\x20-\x7E]/g, '_');
const safeName = req.params.file.replace(/[^\x20-\x7E]/g, '_').replace(/["\\]/g, '_');
res.setHeader('Content-Type', 'audio/midi');
res.setHeader('Content-Disposition', `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(req.params.file)}`);
const stream = fs.createReadStream(filePath);
+72 -1
View File
@@ -106,6 +106,35 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
/* ── Scrollbars ── */
.ac-files::-webkit-scrollbar,.ac-msgs::-webkit-scrollbar{width:3px;}
.ac-files::-webkit-scrollbar-thumb,.ac-msgs::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
/* ── Mobile ── */
@media(max-width:640px){
.ac-resize{display:none;}
.ac-sb{
position:fixed;top:0;left:0;bottom:0;z-index:500;
width:80vw;max-width:300px;
transform:translateX(-100%);transition:transform .25s ease;
box-shadow:4px 0 24px rgba(0,0,0,.4);
}
.ac-sb.mob-open{transform:translateX(0);}
.ac-chat{
position:fixed;top:0;right:0;bottom:0;z-index:500;
width:92vw;max-width:380px;
transform:translateX(100%);transition:transform .25s ease;
box-shadow:-4px 0 24px rgba(0,0,0,.4);
}
.ac-chat.mob-open{transform:translateX(0);}
#mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:499;}
#mob-backdrop.show{display:block;}
.ac-center{width:100%;}
.ac-center-body{padding-bottom:64px;}
#mob-tabbar{display:flex !important;position:fixed;bottom:0;left:0;right:0;z-index:400;background:var(--panel);border-top:1px solid var(--line);height:52px;}
.ac-textarea,input,textarea{font-size:16px !important;}
}
@media(min-width:641px){
#mob-tabbar{display:none !important;}
#mob-backdrop{display:none !important;}
}
</style>
</head>
<body>
@@ -119,6 +148,23 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
<button class="ac-hdr-btn" onclick="toggleTheme()">🌙 테마</button>
</header>
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
<div id="mob-tabbar" style="display:none">
<button class="mob-tab" id="mobtab-files" onclick="mobToggle('files')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<span>파일</span>
</button>
<button class="mob-tab" id="mobtab-home" onclick="mobToggle('home')" style="color:var(--brand,#22c55e);border-top:2px solid var(--brand,#22c55e)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M3 12L12 3l9 9"/><path d="M9 21V12h6v9"/></svg>
<span>홈</span>
</button>
<button class="mob-tab" id="mobtab-chat" onclick="mobToggle('chat')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span>AI</span>
</button>
</div>
<div class="ac-layout">
<!-- ── Sidebar ── -->
@@ -194,6 +240,26 @@ async function checkAuth(){
catch{location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}
}
// ── Mobile overlay ────────────────────────────────────────────────────────────
function mobCloseAll(){
document.querySelector('.ac-sb').classList.remove('mob-open');
document.getElementById('chat-panel').classList.remove('mob-open');
document.getElementById('mob-backdrop').classList.remove('show');
['files','chat'].forEach(t=>document.getElementById('mobtab-'+t)?.classList.remove('active'));
document.getElementById('mobtab-home').style.cssText='color:var(--brand,#22c55e);border-top:2px solid var(--brand,#22c55e)';
}
function mobToggle(which){
const sb=document.querySelector('.ac-sb');
const cp=document.getElementById('chat-panel');
const bd=document.getElementById('mob-backdrop');
if(which==='home'){mobCloseAll();return;}
const sbOpen=sb.classList.contains('mob-open');
const cpOpen=cp.classList.contains('mob-open');
mobCloseAll();
if(which==='files'&&!sbOpen){sb.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-files').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
if(which==='chat'&&!cpOpen){cp.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-chat').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
}
// ── Theme ─────────────────────────────────────────────────────────────────────
function toggleTheme(){const d=document.documentElement;const n=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',n);try{localStorage.setItem('cherryclaw_theme',n);}catch{}}
@@ -369,11 +435,16 @@ async function sendMessage(){
const msg=input.value.trim();
if(!msg||document.getElementById('send-btn').disabled)return;
input.value='';autoResize(input);
if(window.innerWidth<=640) mobToggle('chat');
addMsg('user',escHtml(msg));
await callChat(msg);
}
function sendQuick(msg){document.getElementById('chat-input').value=msg;sendMessage();}
function sendQuick(msg){
if(window.innerWidth<=640) mobCloseAll();
document.getElementById('chat-input').value=msg;
sendMessage();
}
async function callChat(message){
const btn=document.getElementById('send-btn');
+107
View File
@@ -631,6 +631,49 @@ button.danger:hover { background: #fecaca; }
.theme-dot:hover { transform: scale(1.2); }
.theme-dot.active { border-color: var(--text); }
/* ── Mobile ── */
@media(max-width:640px){
.header{flex-wrap:wrap;padding:6px 10px;gap:6px;}
.header h1{font-size:13px;}
.theme-picker{display:none;}
.header-actions{gap:5px;}
.header-actions button{padding:4px 8px;font-size:11px;}
/* Side panels → overlays */
.left-panel{
position:fixed;top:0;left:0;bottom:0;z-index:500;
width:82vw;max-width:300px;
transform:translateX(-100%);transition:transform .25s ease;
box-shadow:4px 0 28px rgba(0,0,0,.5);
resize:none;
}
.left-panel.mob-open{transform:translateX(0);}
.right-panel{
position:fixed;top:0;right:0;bottom:0;z-index:500;
width:92vw;max-width:380px;
transform:translateX(100%);transition:transform .25s ease;
box-shadow:-4px 0 28px rgba(0,0,0,.5);
}
.right-panel.mob-open{transform:translateX(0);}
#mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:499;}
#mob-backdrop.show{display:block;}
/* Bottom tabbar */
#mob-tabbar{display:flex !important;}
/* Center: leave bottom room for tabbar */
.main{padding-bottom:52px;}
.center-panel{border-right:none;}
/* iOS zoom */
input,textarea,select{font-size:16px !important;}
}
@media(min-width:641px){
#mob-backdrop{display:none !important;}
#mob-tabbar{display:none !important;}
}
/* Image lightbox */
#lightbox {
display: none;
@@ -651,6 +694,19 @@ button.danger:hover { background: #fecaca; }
</head>
<body>
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
<div id="mob-tabbar" style="display:none;position:fixed;bottom:0;left:0;right:0;height:52px;background:var(--panel);border-top:1px solid var(--border);z-index:490;align-items:stretch;">
<button class="mob-tab" id="mobtab-ctrl" onclick="mobToggle('left')" style="flex:1;border:none;border-radius:0;background:none;color:var(--muted);font-size:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:0;cursor:pointer;">
<span style="font-size:18px">⚙</span>컨트롤
</button>
<button class="mob-tab" id="mobtab-log" onclick="mobCloseAll()" style="flex:1;border:none;border-radius:0;background:none;color:var(--accent);font-size:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:0;cursor:pointer;">
<span style="font-size:18px">📋</span>로그
</button>
<button class="mob-tab" id="mobtab-panel" onclick="mobToggle('right')" style="flex:1;border:none;border-radius:0;background:none;color:var(--muted);font-size:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:0;cursor:pointer;">
<span style="font-size:18px">🖼</span>패널
</button>
</div>
<!-- Header -->
<div class="header">
<h1>🦷 치과 사전 이미지 에이전트</h1>
@@ -676,6 +732,10 @@ button.danger:hover { background: #fecaca; }
<!-- Left: stats + controls -->
<div class="left-panel">
<div class="panel-section" id="left-mob-close" style="display:none;padding:8px 12px;">
<button onclick="mobCloseAll()" style="width:100%;background:none;border:1px solid var(--border);color:var(--muted);font-size:12px;">✕ 닫기</button>
</div>
<div class="panel-section">
<h2>전체 현황</h2>
<div class="stat-row">
@@ -867,6 +927,10 @@ button.danger:hover { background: #fecaca; }
<!-- Right: tabbed panel -->
<div class="right-panel">
<div style="display:none;padding:6px 10px;border-bottom:1px solid var(--border);background:var(--panel);" id="right-mob-close">
<button onclick="mobCloseAll()" style="width:100%;background:none;border:1px solid var(--border);color:var(--muted);font-size:12px;">✕ 닫기</button>
</div>
<div class="r-tabs">
<div class="r-tab active" onclick="setRTab('gallery')">🖼 갤러리</div>
<div class="r-tab" onclick="setRTab('search')">🔍 검색</div>
@@ -1560,6 +1624,49 @@ function appendChatMsg(text, role) {
return el;
}
// ── Mobile ────────────────────────────────────────────
function mobCloseAll(){
document.querySelector('.left-panel').classList.remove('mob-open');
document.querySelector('.right-panel').classList.remove('mob-open');
document.getElementById('mob-backdrop').classList.remove('show');
document.querySelectorAll('#mob-tabbar .mob-tab').forEach(t=>t.style.color='var(--muted)');
const logTab=document.getElementById('mobtab-log');
if(logTab)logTab.style.color='var(--accent)';
// hide close buttons
const lc=document.getElementById('left-mob-close');
const rc=document.getElementById('right-mob-close');
if(lc)lc.style.display='none';
if(rc)rc.style.display='none';
}
function mobToggle(side){
const isMob=window.innerWidth<=640;
if(!isMob)return;
const leftOpen=document.querySelector('.left-panel').classList.contains('mob-open');
const rightOpen=document.querySelector('.right-panel').classList.contains('mob-open');
if(side==='left'&&leftOpen){mobCloseAll();return;}
if(side==='right'&&rightOpen){mobCloseAll();return;}
mobCloseAll();
if(side==='left'){
document.querySelector('.left-panel').classList.add('mob-open');
document.getElementById('mob-backdrop').classList.add('show');
const lc=document.getElementById('left-mob-close');
if(lc)lc.style.display='block';
const t=document.getElementById('mobtab-ctrl');
if(t)t.style.color='var(--accent)';
const lt=document.getElementById('mobtab-log');
if(lt)lt.style.color='var(--muted)';
} else {
document.querySelector('.right-panel').classList.add('mob-open');
document.getElementById('mob-backdrop').classList.add('show');
const rc=document.getElementById('right-mob-close');
if(rc)rc.style.display='block';
const t=document.getElementById('mobtab-panel');
if(t)t.style.color='var(--accent)';
const lt=document.getElementById('mobtab-log');
if(lt)lt.style.color='var(--muted)';
}
}
// ── Init ──────────────────────────────────────────────
(async () => {
await Promise.all([refreshStats(), loadRecent(), loadModelInfo(), loadVerifyModels()]);
+72 -1
View File
@@ -131,6 +131,34 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
.iv-send{background:#14532d;border:none;border-radius:8px;padding:7px 13px;font-size:12px;color:#fff;cursor:pointer;font-family:inherit;font-weight:700;transition:.15s;flex-shrink:0;}
.iv-send:hover:not(:disabled){background:#166534;}
.iv-send:disabled{opacity:.5;cursor:not-allowed;}
/* ── Mobile ── */
@media(max-width:640px){
.iv-resize{display:none;}
.iv-sb{
position:fixed;top:0;left:0;bottom:0;z-index:500;
width:80vw;max-width:300px;
transform:translateX(-100%);transition:transform .25s ease;
box-shadow:4px 0 24px rgba(0,0,0,.4);
}
.iv-sb.mob-open{transform:translateX(0);}
.iv-chat{
position:fixed;top:0;right:0;bottom:0;z-index:500;
width:92vw;max-width:380px;
transform:translateX(100%);transition:transform .25s ease;
box-shadow:-4px 0 24px rgba(0,0,0,.4);
}
.iv-chat.mob-open{transform:translateX(0);}
#mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:499;}
#mob-backdrop.show{display:block;}
.iv-center{width:100%;}
#mob-tabbar{display:flex !important;position:fixed;bottom:0;left:0;right:0;z-index:400;background:var(--panel);border-top:1px solid var(--line);height:52px;}
.iv-textarea,input,textarea{font-size:16px !important;}
}
@media(min-width:641px){
#mob-tabbar{display:none !important;}
#mob-backdrop{display:none !important;}
}
</style>
</head>
<body>
@@ -143,6 +171,23 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
<button class="hdr-btn" onclick="toggleTheme()">🌙 테마</button>
</header>
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
<div id="mob-tabbar" style="display:none">
<button class="mob-tab" id="mobtab-stocks" onclick="mobToggle('stocks')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
<span>종목</span>
</button>
<button class="mob-tab" id="mobtab-home" onclick="mobToggle('home')" style="color:var(--brand,#22c55e);border-top:2px solid var(--brand,#22c55e)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><rect x="2" y="3" width="20" height="14" rx="2"/><polyline points="8 21 12 17 16 21"/></svg>
<span>차트</span>
</button>
<button class="mob-tab" id="mobtab-chat" onclick="mobToggle('chat')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span>AI</span>
</button>
</div>
<div class="iv-layout">
<!-- ── Sidebar ── -->
@@ -420,6 +465,26 @@ async function checkAuth(){
// ── Theme ─────────────────────────────────────────────────────────────────────
function toggleTheme(){const d=document.documentElement;const n=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',n);try{localStorage.setItem('cherryclaw_theme',n);}catch{}}
// ── Mobile overlay ────────────────────────────────────────────────────────────
function mobCloseAll(){
document.querySelector('.iv-sb').classList.remove('mob-open');
document.getElementById('chat-panel').classList.remove('mob-open');
document.getElementById('mob-backdrop').classList.remove('show');
['stocks','chat'].forEach(t=>document.getElementById('mobtab-'+t)?.classList.remove('active'));
document.getElementById('mobtab-home').style.cssText='color:var(--brand,#22c55e);border-top:2px solid var(--brand,#22c55e)';
}
function mobToggle(which){
const sb=document.querySelector('.iv-sb');
const cp=document.getElementById('chat-panel');
const bd=document.getElementById('mob-backdrop');
if(which==='home'){mobCloseAll();return;}
const sbOpen=sb.classList.contains('mob-open');
const cpOpen=cp.classList.contains('mob-open');
mobCloseAll();
if(which==='stocks'&&!sbOpen){sb.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-stocks').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
if(which==='chat'&&!cpOpen){cp.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-chat').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
}
// ── Resize ────────────────────────────────────────────────────────────────────
(function(){
const handle=document.getElementById('resize-handle');
@@ -479,6 +544,7 @@ function showEmpty(){
// Korean stock via Naver Finance image
function loadKStock(code,name,period){
if(window.innerWidth<=640) mobCloseAll();
period=period||document.getElementById('period-sel').value||'day';
setChartType('kr');
document.getElementById('sym-input').value=code;
@@ -504,6 +570,7 @@ function loadKStock(code,name,period){
// TradingView widget
function loadTV(symbol,interval,name){
if(window.innerWidth<=640) mobCloseAll();
interval=interval||'D';
setChartType('tv');
document.getElementById('sym-input').value=symbol;
@@ -654,11 +721,15 @@ async function sendMessage(){
const msg=input.value.trim();
if(!msg||document.getElementById('send-btn').disabled)return;
input.value='';autoResize(input);
if(window.innerWidth<=640) mobToggle('chat');
addMsg('user',escHtml(msg));
await callChat(msg);
}
function sendQuick(msg){addMsg('user',escHtml(msg));callChat(msg);}
function sendQuick(msg){
if(window.innerWidth<=640){mobCloseAll();}
addMsg('user',escHtml(msg));callChat(msg);
}
async function callChat(message){
const btn=document.getElementById('send-btn');
+103
View File
@@ -163,6 +163,60 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }
/* ── Mobile ── */
@media(max-width:640px){
/* Header: wrap to 2 rows */
.lg-header { flex-wrap:wrap; padding:6px 10px; gap:5px; }
.lg-header h1 { font-size:13px; }
.lg-header .sub { display:none; }
/* hide theme & voice on mobile (secondary settings) */
.lg-header > div:has(.lg-theme-btn),
.lg-header > div:has(.tts-gender) { display:none; }
.lg-header > a[href="/"] { display:none; }
/* stat chip stays, moves to right */
/* Sidebar: overlay drawer */
.lg-sidebar {
position:fixed;top:0;left:0;bottom:0;z-index:500;
width:80vw;max-width:280px;
transform:translateX(-100%);transition:transform .25s ease;
box-shadow:4px 0 24px rgba(0,0,0,.45);
}
.lg-sidebar.mob-open { transform:translateX(0); }
/* hide sidebar nav tabs on mobile — bottom tabbar replaces them */
.csb-tab-bar { display:none; }
/* all sb-* content visible in drawer (setNav handles show/hide) */
#mob-backdrop { display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:499; }
#mob-backdrop.show { display:block; }
/* Main takes full width */
.lg-main { width:100%; }
/* bottom padding so content clears tabbar */
.card-wrap, .vocab-wrap, .quiz-wrap, .lessons-grid, .lesson-steps { padding-bottom:64px; }
/* Bottom tabbar */
#mob-tabbar {
display:flex !important;
position:fixed;bottom:0;left:0;right:0;z-index:400;
background:var(--panel);border-top:1px solid var(--line);
height:52px;
}
/* Add filter button to toolbar (hidden on desktop) */
.mob-filter-btn { display:flex !important; }
/* iOS zoom prevention */
input, textarea, select { font-size:16px !important; }
.quiz-fill { font-size:16px !important; }
}
@media(min-width:641px){
#mob-tabbar { display:none !important; }
#mob-backdrop { display:none !important; }
.mob-filter-btn { display:none !important; }
}
</style>
</head>
<body>
@@ -187,6 +241,27 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
<a href="/" style="font-size:12px;color:var(--muted);text-decoration:none;margin-left:4px">← 메인</a>
</div>
<div id="mob-backdrop" onclick="mobCloseFilter()"></div>
<div id="mob-tabbar" style="display:none">
<button class="mob-tab" id="mobtab-flash" onclick="mobNav('flash')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><rect x="2" y="4" width="20" height="16" rx="2"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="16" y1="2" x2="16" y2="6"/></svg>
<span>카드</span>
</button>
<button class="mob-tab" id="mobtab-vocab" onclick="mobNav('vocab')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
<span>어휘</span>
</button>
<button class="mob-tab" id="mobtab-quiz" onclick="mobNav('quiz')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
<span>퀴즈</span>
</button>
<button class="mob-tab" id="mobtab-lessons" onclick="mobNav('lessons')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
<span>레슨</span>
</button>
</div>
<div class="lg-layout">
<!-- Sidebar -->
@@ -245,6 +320,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
<!-- ═══ FLASHCARD PANEL ═══ -->
<div class="lg-panel active" id="panel-flash">
<div class="lg-toolbar">
<button class="lg-btn mob-filter-btn" style="display:none;padding:4px 8px" onclick="mobOpenFilter()" title="카테고리">☰</button>
<span class="lg-toolbar-title" id="flash-title">플래시카드 — 전체</span>
<span style="font-size:12px;color:var(--muted)" id="flash-counter">0 / 0</span>
<label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--muted);cursor:pointer">
@@ -281,6 +357,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
<!-- ═══ VOCAB PANEL ═══ -->
<div class="lg-panel" id="panel-vocab">
<div class="search-row">
<button class="lg-btn mob-filter-btn" style="display:none;padding:4px 8px;flex-shrink:0" onclick="mobOpenFilter()" title="필터">☰</button>
<input type="text" id="vocab-search" placeholder="단어 검색..." oninput="renderVocabTable()">
<label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--muted);cursor:pointer;white-space:nowrap">
<input type="checkbox" id="vocab-show-rom" checked onchange="renderVocabTable()" style="accent-color:#22c55e">발음
@@ -305,6 +382,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
<!-- ═══ QUIZ PANEL ═══ -->
<div class="lg-panel" id="panel-quiz">
<div class="lg-toolbar">
<button class="lg-btn mob-filter-btn" style="display:none;padding:4px 8px" onclick="mobOpenFilter()" title="퀴즈 설정">☰</button>
<span class="lg-toolbar-title">퀴즈</span>
<select class="lg-select" id="quiz-type-sel">
<option value="multiple">객관식</option>
@@ -365,6 +443,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
<!-- Lesson grid view -->
<div id="lessons-grid-view" style="display:flex;flex-direction:column;flex:1;overflow:hidden">
<div class="lg-toolbar">
<button class="lg-btn mob-filter-btn" style="display:none;padding:4px 8px" onclick="mobOpenFilter()" title="레슨 목록">☰</button>
<span class="lg-toolbar-title">레슨</span>
<button class="lg-btn" onclick="resetLessons()">↺ 진도 초기화</button>
</div>
@@ -392,6 +471,30 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
</div><!-- /lg-layout -->
<script>
// ── Mobile ────────────────────────────────────────────────────────────────────
function mobCloseFilter(){
document.querySelector('.lg-sidebar').classList.remove('mob-open');
document.getElementById('mob-backdrop').classList.remove('show');
}
function mobOpenFilter(){
document.querySelector('.lg-sidebar').classList.add('mob-open');
document.getElementById('mob-backdrop').classList.add('show');
}
function mobNav(nav){
mobCloseFilter();
setNav(nav);
// update bottom tab active state
['flash','vocab','quiz','lessons'].forEach(n=>{
const t=document.getElementById('mobtab-'+n);
if(t) t.classList.toggle('active', n===nav);
});
}
// hook category/filter buttons to auto-close sidebar on mobile
['setCategory','setVocabCat','setQuizCat'].forEach(fn=>{
const orig=window[fn];
if(orig) window[fn]=function(...a){orig(...a);if(window.innerWidth<=640)mobCloseFilter();};
});
// ═══════════════════════════════════════════════════════
// DATA
// ═══════════════════════════════════════════════════════
+72 -1
View File
@@ -131,6 +131,35 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
.lw-send{background:#92400e;border:none;border-radius:8px;padding:7px 13px;font-size:12px;color:#fff;cursor:pointer;font-family:inherit;font-weight:700;transition:.15s;flex-shrink:0;}
.lw-send:hover:not(:disabled){background:#b45309;}
.lw-send:disabled{opacity:.5;cursor:not-allowed;}
/* ── Mobile ── */
@media(max-width:640px){
.lw-resize{display:none;}
.lw-sb{
position:fixed;top:0;left:0;bottom:0;z-index:500;
width:80vw;max-width:300px;
transform:translateX(-100%);transition:transform .25s ease;
box-shadow:4px 0 24px rgba(0,0,0,.4);
}
.lw-sb.mob-open{transform:translateX(0);}
.lw-chat{
position:fixed;top:0;right:0;bottom:0;z-index:500;
width:92vw;max-width:380px;
transform:translateX(100%);transition:transform .25s ease;
box-shadow:-4px 0 24px rgba(0,0,0,.4);
}
.lw-chat.mob-open{transform:translateX(0);}
#mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:499;}
#mob-backdrop.show{display:block;}
.lw-center{width:100%;}
.lw-center-body{padding-bottom:64px;}
#mob-tabbar{display:flex !important;position:fixed;bottom:0;left:0;right:0;z-index:400;background:var(--panel);border-top:1px solid var(--line);height:52px;}
.lw-textarea,input,textarea{font-size:16px !important;}
}
@media(min-width:641px){
#mob-tabbar{display:none !important;}
#mob-backdrop{display:none !important;}
}
</style>
</head>
<body>
@@ -143,6 +172,23 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
<button class="hdr-btn" onclick="toggleTheme()">🌙 테마</button>
</header>
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
<div id="mob-tabbar" style="display:none">
<button class="mob-tab" id="mobtab-quick" onclick="mobToggle('quick')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<span>빠른질문</span>
</button>
<button class="mob-tab" id="mobtab-home" onclick="mobToggle('home')" style="color:var(--brand,#eab308);border-top:2px solid var(--brand,#eab308)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<span>검색</span>
</button>
<button class="mob-tab" id="mobtab-chat" onclick="mobToggle('chat')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span>AI</span>
</button>
</div>
<div class="lw-layout">
<!-- ── Sidebar ── -->
@@ -249,6 +295,26 @@ async function checkAuth(){
// ── Theme ─────────────────────────────────────────────────────────────────────
function toggleTheme(){const d=document.documentElement;const n=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',n);try{localStorage.setItem('cherryclaw_theme',n);}catch{}}
// ── Mobile overlay ────────────────────────────────────────────────────────────
function mobCloseAll(){
document.querySelector('.lw-sb').classList.remove('mob-open');
document.getElementById('chat-panel').classList.remove('mob-open');
document.getElementById('mob-backdrop').classList.remove('show');
['quick','chat'].forEach(t=>document.getElementById('mobtab-'+t)?.classList.remove('active'));
document.getElementById('mobtab-home').style.cssText='color:var(--brand,#eab308);border-top:2px solid var(--brand,#eab308)';
}
function mobToggle(which){
const sb=document.querySelector('.lw-sb');
const cp=document.getElementById('chat-panel');
const bd=document.getElementById('mob-backdrop');
if(which==='home'){mobCloseAll();return;}
const sbOpen=sb.classList.contains('mob-open');
const cpOpen=cp.classList.contains('mob-open');
mobCloseAll();
if(which==='quick'&&!sbOpen){sb.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-quick').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
if(which==='chat'&&!cpOpen){cp.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-chat').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
}
// ── Resize ────────────────────────────────────────────────────────────────────
(function(){
const CHAT_W='lw_chat_width';
@@ -350,6 +416,7 @@ function renderResults(data,query){
}
async function selectResult(idx){
if(window.innerWidth<=640) mobCloseAll();
document.querySelectorAll('.lw-result-item').forEach(el=>el.classList.remove('active'));
const el=document.getElementById('ri-'+idx);
if(el)el.classList.add('active');
@@ -446,11 +513,15 @@ async function sendMessage(){
const msg=input.value.trim();
if(!msg||document.getElementById('send-btn').disabled)return;
input.value='';autoResize(input);
if(window.innerWidth<=640) mobToggle('chat');
addMsg('user',escHtml(msg));
await callChat(msg);
}
function sendQuick(msg){addMsg('user',escHtml(msg));callChat(msg);}
function sendQuick(msg){
if(window.innerWidth<=640) mobCloseAll();
addMsg('user',escHtml(msg));callChat(msg);
}
async function callChat(message){
const btn=document.getElementById('send-btn');
+89
View File
@@ -155,6 +155,54 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
.mn-resize{width:4px;flex-shrink:0;background:var(--line);cursor:col-resize;transition:.15s;}
.mn-resize:hover,.mn-resize.dragging{background:#8b5cf6;}
/* ── Mobile ── */
@media(max-width:640px){
.mn-hdr{flex-wrap:wrap;padding:5px 8px;gap:5px;}
.mn-hdr h1{font-size:12px;}
.mode-toggle{order:10;flex:0 0 100%;}
.mode-tab{flex:1;font-size:10px;padding:4px 0;}
.mn-hdr-sp{display:none;}
/* hide desktop resize */
.mn-resize{display:none;}
/* sidebar & chat become fixed overlays */
.mn-sb{
position:fixed;top:0;left:0;bottom:0;z-index:500;
width:80vw;max-width:300px;
transform:translateX(-100%);transition:transform .25s ease;
border-right:1px solid var(--line);
box-shadow:4px 0 24px rgba(0,0,0,.4);
}
.mn-sb.mob-open{transform:translateX(0);}
.mn-chat{
position:fixed;top:0;right:0;bottom:0;z-index:500;
width:92vw;max-width:380px;
transform:translateX(100%);transition:transform .25s ease;
border-left:1px solid var(--line);
box-shadow:-4px 0 24px rgba(0,0,0,.4);
}
.mn-chat.mob-open{transform:translateX(0);}
/* overlay backdrop */
#mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:499;}
#mob-backdrop.show{display:block;}
/* center takes full width */
.mn-center{width:100%;}
/* bottom tab bar */
#mob-tabbar{
display:flex !important;
position:fixed;bottom:0;left:0;right:0;z-index:400;
background:var(--panel);border-top:1px solid var(--line);
height:52px;
}
/* add bottom padding so content isn't hidden behind tab bar */
.mn-center-body{padding-bottom:64px;}
/* font-size ≥16px to prevent iOS auto-zoom */
.mn-textarea,input,textarea{font-size:16px !important;}
}
@media(min-width:641px){
#mob-tabbar{display:none !important;}
#mob-backdrop{display:none !important;}
}
/* ── Chat ── */
.mn-chat{width:340px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid var(--line);background:var(--panel);min-height:0;}
.mn-chat-hdr{display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid var(--line);flex-shrink:0;gap:6px;}
@@ -210,6 +258,23 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
<button class="hdr-btn" onclick="toggleTheme()">🌙 테마</button>
</header>
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
<div id="mob-tabbar" style="display:none">
<button class="mob-tab" id="mobtab-tools" onclick="mobToggle('tools')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
<span>도구</span>
</button>
<button class="mob-tab" id="mobtab-home" onclick="mobToggle('home')" style="color:var(--brand,#8b5cf6);border-top:2px solid var(--brand,#8b5cf6)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M3 12L12 3l9 9"/><path d="M9 21V12h6v9"/></svg>
<span>홈</span>
</button>
<button class="mob-tab" id="mobtab-chat" onclick="mobToggle('chat')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span>AI 상담</span>
</button>
</div>
<div class="mn-layout">
<!-- ── Sidebar ── -->
@@ -314,6 +379,26 @@ async function checkAuth(){
catch{location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}
}
// ── Mobile overlay ────────────────────────────────────────────────────────────
function mobCloseAll(){
document.querySelector('.mn-sb').classList.remove('mob-open');
document.getElementById('chat-panel').classList.remove('mob-open');
document.getElementById('mob-backdrop').classList.remove('show');
['tools','chat'].forEach(t=>document.getElementById('mobtab-'+t)?.classList.remove('active'));
document.getElementById('mobtab-home').style.cssText='color:var(--brand,#8b5cf6);border-top:2px solid var(--brand,#8b5cf6)';
}
function mobToggle(which){
const sb=document.querySelector('.mn-sb');
const cp=document.getElementById('chat-panel');
const bd=document.getElementById('mob-backdrop');
if(which==='home'){mobCloseAll();return;}
const isSbOpen=sb.classList.contains('mob-open');
const isCpOpen=cp.classList.contains('mob-open');
mobCloseAll();
if(which==='tools'&&!isSbOpen){sb.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-tools').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
if(which==='chat'&&!isCpOpen){cp.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-chat').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
}
// ── Theme ─────────────────────────────────────────────────────────────────────
function toggleTheme(){const d=document.documentElement;const n=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',n);try{localStorage.setItem('cherryclaw_theme',n);}catch{}}
@@ -408,6 +493,8 @@ const toolTitles={
};
function showTool(name){
// on mobile: close sidebar overlay after selecting a tool
if(window.innerWidth<=640) mobCloseAll();
// stop breath timer if switching
if(breathTimer&&name!=='breath'){clearInterval(breathTimer);breathTimer=null;}
activeTool=name;
@@ -931,6 +1018,8 @@ async function sendMessage(){
const msg=input.value.trim();
if(!msg||document.getElementById('send-btn').disabled)return;
input.value='';autoResize(input);
// on mobile: open chat panel so user can see the response
if(window.innerWidth<=640) mobToggle('chat');
addMsg('user',escHtml(msg));
await callChat(msg);
}
+878 -260
View File
File diff suppressed because it is too large Load Diff
+85 -2
View File
@@ -258,6 +258,66 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
.sed-img-thumb{width:100%;height:100px;border-radius:6px;background:var(--panel) center/cover no-repeat;border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--muted);cursor:pointer;transition:.15s;}
.sed-img-thumb:hover{border-color:var(--brand);}
.sed-nav-info{font-size:12px;color:var(--muted);text-align:center;flex:1;}
/* ── Mobile ── */
@media(max-width:640px){
/* Header: compact */
.header{flex-wrap:wrap;padding:7px 12px;gap:6px;}
.header h1{font-size:13px;}
/* hide theme buttons — they're inside sidebar on mobile */
.header > div:has(.theme-btn){display:none;}
/* step dots: hide text labels, keep numbers */
.sdot{padding:5px 8px;font-size:0;}
.sdot .n{font-size:10px;}
.step-hint{display:none;}
.steps{gap:0;margin-left:auto;}
/* Sidebar: overlay drawer */
.sidebar{
position:fixed;top:0;left:0;bottom:0;z-index:500;
width:82vw;max-width:310px;
transform:translateX(-100%);transition:transform .25s ease;
box-shadow:4px 0 28px rgba(0,0,0,.5);
order:0;
}
.sidebar.mob-open{transform:translateX(0);}
/* theme buttons inside sidebar on mobile */
.sidebar-mob-theme{display:flex !important;}
#mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:499;}
#mob-backdrop.show{display:block;}
/* settings toggle button */
.mob-cfg-btn{display:inline-flex !important;}
/* Step scroll area: bottom padding for bbar */
.sv .scroll{padding-bottom:16px;}
/* Search row: wrap */
.srow{flex-wrap:wrap;}
.srow input{min-width:0;}
/* Source checkboxes: narrower */
.src-chk{width:calc(50% - 4px);min-width:0;}
/* Step 2: stack layout */
#step-2 .scroll > div[style*="flex:2"],
#step-2 .scroll > div[style*="flex:1"]{flex:1 1 100%;}
#step-2 .scroll{flex-wrap:wrap;}
/* Slide editor modal: full screen */
.sedmodal{width:100vw !important;height:100dvh !important;max-width:100vw;max-height:100dvh;border-radius:0;left:0;top:0;transform:none;}
.sed-body{flex-direction:column;}
.sed-ctrl{width:100%;max-height:40vh;border-right:none;border-bottom:1px solid var(--line);}
/* iOS zoom prevention */
input,textarea,select{font-size:16px !important;}
}
@media(min-width:641px){
#mob-backdrop{display:none !important;}
.mob-cfg-btn{display:none !important;}
.sidebar-mob-theme{display:none !important;}
}
</style>
</head>
<body>
@@ -265,6 +325,7 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
<div class="header">
<span style="font-size:18px">📊</span>
<h1>슬라이드 제작 마법사</h1>
<button class="mob-cfg-btn theme-btn" style="display:none;padding:5px 10px" onclick="mobToggleSidebar()">⚙ 설정</button>
<div style="display:flex;gap:4px;margin-left:14px">
<button class="theme-btn active" id="tbtn-dark" onclick="setTheme('dark')">🌙 다크</button>
<button class="theme-btn" id="tbtn-light" onclick="setTheme('light')">☀️ 라이트</button>
@@ -279,6 +340,8 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
</div>
</div>
<div id="mob-backdrop" onclick="mobCloseSidebar()"></div>
<div class="body">
<!-- ── STEP 1 ── -->
@@ -429,7 +492,15 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
<!-- ── SETTINGS SIDEBAR ── -->
<div class="sidebar">
<div class="ptitle">설정</div>
<div class="ptitle" style="display:flex;align-items:center;justify-content:space-between">
<span>설정</span>
<div class="sidebar-mob-theme" style="display:none;gap:4px;align-items:center">
<button class="theme-btn" id="tbtn-dark2" onclick="setTheme('dark')" style="padding:2px 8px;font-size:11px">🌙</button>
<button class="theme-btn" id="tbtn-light2" onclick="setTheme('light')" style="padding:2px 8px;font-size:11px">☀️</button>
<button class="theme-btn" id="tbtn-warm2" onclick="setTheme('warm')" style="padding:2px 8px;font-size:11px">🌿</button>
<button onclick="mobCloseSidebar()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:16px;padding:0 4px;line-height:1">✕</button>
</div>
</div>
<div class="scroll">
<div class="field">
<button class="btn bgreen" style="width:100%;font-size:12px;padding:7px 0" onclick="newProject()">+ 새 프로젝트</button>
@@ -744,11 +815,23 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
'use strict';
// ── Theme ────────────────────────────────────────────────────────────────────
// ── Mobile sidebar ────────────────────────────────────────────────────────────
function mobCloseSidebar(){
document.querySelector('.sidebar').classList.remove('mob-open');
document.getElementById('mob-backdrop').classList.remove('show');
}
function mobToggleSidebar(){
const sb=document.querySelector('.sidebar');
const open=sb.classList.contains('mob-open');
if(open){mobCloseSidebar();}
else{sb.classList.add('mob-open');document.getElementById('mob-backdrop').classList.add('show');}
}
function setTheme(name) {
document.body.classList.remove('theme-light','theme-warm');
if(name==='light') document.body.classList.add('theme-light');
if(name==='warm') document.body.classList.add('theme-warm');
document.querySelectorAll('.theme-btn').forEach(b=>b.classList.toggle('active',b.id==='tbtn-'+name));
document.querySelectorAll('.theme-btn').forEach(b=>b.classList.toggle('active',b.id==='tbtn-'+name||b.id==='tbtn-'+name+'2'));
localStorage.setItem('pptx-wizard-theme',name);
}
(function(){ setTheme(localStorage.getItem('pptx-wizard-theme')||'dark'); })();