feat: 글자크기를 계정별·메뉴별로 저장(재로그인 유지)

기존 전역 localStorage 방식(모든 메뉴 일괄 적용)을 화면 경로(메뉴키) 기준
계정별 DB 저장으로 변경. A−/A+ 조정 시 해당 화면만 조정·저장되고, 다음
방문·재로그인 시 각 화면의 저장값이 서버 주입으로 복원된다.

- member_menu_font_scale 테이블 + 저장 엔드포인트(bag/pref/font-scale)
- member_font_scale_for() 헬퍼로 렌더 시 현재 화면 저장값 주입
- admin/portal/embed 레이아웃 + 워크스페이스(탭 iframe)에 적용
  · 워크스페이스는 포커스된 탭(메뉴)만 조정, 디바운스 저장으로 CSRF 경합 방지

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-07-13 11:12:43 +09:00
parent 62f7bda290
commit 03c2bbc05c
8 changed files with 210 additions and 47 deletions

View File

@@ -63,6 +63,7 @@ $routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void
$routes->get('manual/search', 'Bag::manualSearch'); // (:segment) 보다 먼저 $routes->get('manual/search', 'Bag::manualSearch'); // (:segment) 보다 먼저
$routes->get('manual/(:segment)', 'Bag::manualPage/$1'); $routes->get('manual/(:segment)', 'Bag::manualPage/$1');
$routes->post('activity/print-log', 'Bag::printLog'); // 인쇄 활동 기록(beacon) $routes->post('activity/print-log', 'Bag::printLog'); // 인쇄 활동 기록(beacon)
$routes->post('pref/font-scale', 'Bag::saveFontScale'); // 계정별·메뉴별 글자크기 저장
}); });
$routes->get('bag/number-lookup', 'Bag::numberLookup'); $routes->get('bag/number-lookup', 'Bag::numberLookup');

View File

@@ -224,6 +224,56 @@ class Bag extends BaseController
]); ]);
} }
/**
* 계정별·메뉴별 글자크기(zoom %) 저장. AJAX 전용.
* menu_key(화면 경로) + scale(70~150)을 로그인 계정 기준으로 upsert.
*/
public function saveFontScale(): ResponseInterface
{
$mbIdx = (int) (session()->get('mb_idx') ?? 0);
if ($mbIdx <= 0) {
return $this->response->setJSON(['ok' => false, 'csrf' => csrf_hash()]);
}
$menuKey = trim((string) ($this->request->getPost('menu_key') ?? ''));
$scale = (int) ($this->request->getPost('scale') ?? 0);
if ($menuKey === '' || $scale < 70 || $scale > 150) {
return $this->response->setJSON(['ok' => false, 'csrf' => csrf_hash()]);
}
$menuKey = mb_substr($menuKey, 0, 191);
$db = \Config\Database::connect();
if (! $db->tableExists('member_menu_font_scale')) {
$db->query(<<<'SQL'
CREATE TABLE IF NOT EXISTS `member_menu_font_scale` (
`mmf_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`mmf_mb_idx` INT UNSIGNED NOT NULL,
`mmf_menu_key` VARCHAR(191) NOT NULL,
`mmf_scale` SMALLINT UNSIGNED NOT NULL DEFAULT 100,
`mmf_updated` DATETIME NOT NULL,
PRIMARY KEY (`mmf_idx`),
UNIQUE KEY `uk_mb_menu` (`mmf_mb_idx`,`mmf_menu_key`),
KEY `idx_mb` (`mmf_mb_idx`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL);
}
$now = date('Y-m-d H:i:s');
$exists = $db->table('member_menu_font_scale')
->where('mmf_mb_idx', $mbIdx)->where('mmf_menu_key', $menuKey)->countAllResults();
if ($exists > 0) {
$db->table('member_menu_font_scale')
->where('mmf_mb_idx', $mbIdx)->where('mmf_menu_key', $menuKey)
->update(['mmf_scale' => $scale, 'mmf_updated' => $now]);
} else {
$db->table('member_menu_font_scale')->insert([
'mmf_mb_idx' => $mbIdx, 'mmf_menu_key' => $menuKey,
'mmf_scale' => $scale, 'mmf_updated' => $now,
]);
}
return $this->response->setJSON(['ok' => true, 'csrf' => csrf_hash()]);
}
// ────────────────────────────────────────────── // ──────────────────────────────────────────────
// 기본정보관리 (단가·포장 단위 진입 허브) // 기본정보관리 (단가·포장 단위 진입 허브)
// ────────────────────────────────────────────── // ──────────────────────────────────────────────

View File

@@ -229,6 +229,37 @@ if (! function_exists('current_nav_request_path')) {
} }
} }
if (! function_exists('member_font_scale_for')) {
/**
* 로그인 계정의 특정 메뉴(화면 경로)에 저장된 글자크기(zoom %)를 반환.
* 저장값이 없거나 범위(70~150) 밖이면 100.
*/
function member_font_scale_for(string $menuKey): int
{
$mbIdx = (int) (session()->get('mb_idx') ?? 0);
$menuKey = trim($menuKey);
if ($mbIdx <= 0 || $menuKey === '') {
return 100;
}
try {
$db = \Config\Database::connect();
if (! $db->tableExists('member_menu_font_scale')) {
return 100;
}
$row = $db->table('member_menu_font_scale')
->select('mmf_scale')
->where('mmf_mb_idx', $mbIdx)
->where('mmf_menu_key', $menuKey)
->get()->getRow();
} catch (\Throwable $e) {
return 100;
}
$s = $row ? (int) $row->mmf_scale : 100;
return ($s >= 70 && $s <= 150) ? $s : 100;
}
}
if (! function_exists('normalize_menu_link_for_url')) { if (! function_exists('normalize_menu_link_for_url')) {
/** /**
* menu.mm_link 를 base_url() 인자로 쓸 수 있는 상대 경로로 정규화합니다. * menu.mm_link 를 base_url() 인자로 쓸 수 있는 상대 경로로 정규화합니다.

View File

@@ -62,6 +62,9 @@ $navPartial = [
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title><?= esc($title ?? '관리자') ?> - GBLS</title> <title><?= esc($title ?? '관리자') ?> - GBLS</title>
<link rel="icon" type="image/svg+xml" href="<?= base_url('favicon.svg') ?>"/>
<link rel="alternate icon" href="<?= base_url('favicon.ico') ?>"/>
<link rel="apple-touch-icon" href="<?= base_url('favicon.svg') ?>"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script> <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
@@ -174,27 +177,40 @@ tailwind.config = {
window.addEventListener('pagehide', closeStuckOverlays); window.addEventListener('pagehide', closeStuckOverlays);
})(); })();
// 글씨 크기 조절(A/A+) — 본문 + 상단 대메뉴 + 좌측 사이드바에 zoom 적용. 사이트/워크스페이스와 배율 공유. // 글씨 크기 조절(A/A+) — 계정별·메뉴별 저장(서버). 현재 화면 경로 기준으로 개별 적용.
<?php $__fmk = function_exists('current_nav_request_path') ? current_nav_request_path() : ''; $__fsc = function_exists('member_font_scale_for') ? member_font_scale_for($__fmk) : 100; ?>
(function () { (function () {
var FONT_KEY = 'jrj_font_scale'; var MENU_KEY = <?= json_encode($__fmk) ?>;
// 상단 헤더는 제외 → A/A+ 버튼이 커지거나 밀리지 않아 연속 클릭이 편하다. var scale = <?= (int) $__fsc ?>;
var SAVE_URL = <?= json_encode(site_url('bag/pref/font-scale')) ?>;
var CSRF_NAME = <?= json_encode(csrf_token()) ?>;
var csrfHash = <?= json_encode(csrf_hash()) ?>;
var scaleSelectors = ['.sidebar', '.work-main']; var scaleSelectors = ['.sidebar', '.work-main'];
function curScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; } var saveTimer = null;
function applyScale(s) { function persist(s) {
try { localStorage.setItem('jrj_font_scale::' + MENU_KEY, String(s)); } catch (e) {}
if (!MENU_KEY) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(function () {
var b = new URLSearchParams(); b.set(CSRF_NAME, csrfHash); b.set('menu_key', MENU_KEY); b.set('scale', String(s));
fetch(SAVE_URL, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: b.toString() })
.then(function (r) { return r.json(); }).then(function (d) { if (d && d.csrf) csrfHash = d.csrf; }).catch(function () {});
}, 400);
}
function applyScale(s, save) {
s = Math.min(150, Math.max(70, s)); s = Math.min(150, Math.max(70, s));
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {} scale = s;
var z = s / 100; var z = s / 100;
scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; }); scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; });
// 상단 대메뉴는 font-size 만 키운다(바 높이 48px 고정 → A/A+ 버튼이 안 밀림).
var topnav = document.querySelector('.portal-top-nav'); var topnav = document.querySelector('.portal-top-nav');
if (topnav) topnav.style.fontSize = (0.875 * z).toFixed(4) + 'rem'; if (topnav) topnav.style.fontSize = (0.875 * z).toFixed(4) + 'rem';
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%'; var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
if (save) persist(s);
} }
applyScale(curScale()); applyScale(scale, false);
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus'); var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
if (plus) plus.addEventListener('click', function () { applyScale(curScale() + 10); }); if (plus) plus.addEventListener('click', function () { applyScale(scale + 10, true); });
if (minus) minus.addEventListener('click', function () { applyScale(curScale() - 10); }); if (minus) minus.addEventListener('click', function () { applyScale(scale - 10, true); });
window.addEventListener('storage', function (e) { if (e.key === FONT_KEY) applyScale(curScale()); });
})(); })();
</script> </script>
<script> <script>

View File

@@ -175,12 +175,13 @@ tailwind.config = {
document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; }); document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; });
})(); })();
// 글씨 크기(zoom) — 상단바에서 조절한 값을 적용. localStorage 공유 + storage 이벤트로 실시간 반영. // 글씨 크기(zoom) — 계정별·메뉴별 저장값(서버 주입)을 이 화면에 적용.
function applyFontScale() { <?php $__fmk = function_exists('current_nav_request_path') ? current_nav_request_path() : ''; $__fsc = function_exists('member_font_scale_for') ? member_font_scale_for($__fmk) : 100; ?>
try { var s = parseInt(localStorage.getItem('jrj_font_scale') || '100', 10); if (!(s >= 70 && s <= 150)) s = 100; document.documentElement.style.zoom = (s / 100); } catch (e) {} (function () {
} var s = <?= (int) $__fsc ?>;
applyFontScale(); if (!(s >= 70 && s <= 150)) s = 100;
window.addEventListener('storage', function (e) { if (e.key === 'jrj_font_scale') applyFontScale(); }); try { document.documentElement.style.zoom = (s / 100); } catch (e) {}
})();
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
var a = e.target.closest ? e.target.closest('a[href]') : null; var a = e.target.closest ? e.target.closest('a[href]') : null;

View File

@@ -264,25 +264,39 @@ tailwind.config = {
document.addEventListener('mousemove', function (e) { if (!dragging) return; var w = window.innerWidth - e.clientX; drawer.style.width = Math.min(window.innerWidth * 0.92, Math.max(300, w)) + 'px'; }); document.addEventListener('mousemove', function (e) { if (!dragging) return; var w = window.innerWidth - e.clientX; drawer.style.width = Math.min(window.innerWidth * 0.92, Math.max(300, w)) + 'px'; });
document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; }); document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; });
// 글씨 크기 조절 — 본문 + 좌측 사이드바에 zoom 적용. // 글씨 크기 조절 — 계정별·메뉴별 저장(서버). 현재 화면 경로 기준으로 개별 적용.
// 상단 헤더는 제외 → A/A+ 버튼이 커지거나 밀리지 않아 연속 클릭이 편하다. <?php $__fmk = function_exists('current_nav_request_path') ? current_nav_request_path() : ''; $__fsc = function_exists('member_font_scale_for') ? member_font_scale_for($__fmk) : 100; ?>
var FONT_KEY = 'jrj_font_scale'; var MENU_KEY = <?= json_encode($__fmk) ?>;
var scale = <?= (int) $__fsc ?>;
var SAVE_URL = <?= json_encode(site_url('bag/pref/font-scale')) ?>;
var CSRF_NAME = <?= json_encode(csrf_token()) ?>;
var csrfHash = <?= json_encode(csrf_hash()) ?>;
var scaleSelectors = ['.sidebar', '.work-main']; var scaleSelectors = ['.sidebar', '.work-main'];
function curScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; } var saveTimer = null;
function applyScale(s) { function persist(s) {
try { localStorage.setItem('jrj_font_scale::' + MENU_KEY, String(s)); } catch (e) {}
if (!MENU_KEY) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(function () {
var b = new URLSearchParams(); b.set(CSRF_NAME, csrfHash); b.set('menu_key', MENU_KEY); b.set('scale', String(s));
fetch(SAVE_URL, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: b.toString() })
.then(function (r) { return r.json(); }).then(function (d) { if (d && d.csrf) csrfHash = d.csrf; }).catch(function () {});
}, 400);
}
function applyScale(s, save) {
s = Math.min(150, Math.max(70, s)); s = Math.min(150, Math.max(70, s));
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {} scale = s;
var z = s / 100; var z = s / 100;
scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; }); scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; });
// 상단 대메뉴는 font-size 만 키운다(바 높이 48px 고정 → A/A+ 버튼이 안 밀림).
var topnav = document.querySelector('.portal-top-nav'); var topnav = document.querySelector('.portal-top-nav');
if (topnav) topnav.style.fontSize = (0.875 * z).toFixed(4) + 'rem'; if (topnav) topnav.style.fontSize = (0.875 * z).toFixed(4) + 'rem';
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%'; var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
if (save) persist(s);
} }
applyScale(curScale()); applyScale(scale, false);
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus'); var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
if (plus) plus.addEventListener('click', function () { applyScale(curScale() + 10); }); if (plus) plus.addEventListener('click', function () { applyScale(scale + 10, true); });
if (minus) minus.addEventListener('click', function () { applyScale(curScale() - 10); }); if (minus) minus.addEventListener('click', function () { applyScale(scale - 10, true); });
})(); })();
</script> </script>
<script> <script>

View File

@@ -331,7 +331,7 @@ if ($effectiveLgIdx) {
persist(); persist();
} }
function focusSlot(i) { focused = i; render(); } function focusSlot(i) { focused = i; render(); try { updateFontPct(); } catch (e) {} }
// 포커스 칸에 탭 배치 (이미 다른 칸에 있으면 두 칸을 맞바꿈) // 포커스 칸에 탭 배치 (이미 다른 칸에 있으면 두 칸을 맞바꿈)
function placeInFocused(id) { function placeInFocused(id) {
@@ -343,6 +343,7 @@ if ($effectiveLgIdx) {
else { slots[focused] = id; } else { slots[focused] = id; }
} }
render(); render();
try { updateFontPct(); } catch (e) {}
} }
function setLayout(mode) { function setLayout(mode) {
@@ -423,6 +424,7 @@ if ($effectiveLgIdx) {
d.addEventListener('keydown', handleShortcut); d.addEventListener('keydown', handleShortcut);
d.addEventListener('mousedown', function () { var j = slots.indexOf(id); if (j >= 0 && j !== focused) focusSlot(j); }, true); d.addEventListener('mousedown', function () { var j = slots.indexOf(id); if (j >= 0 && j !== focused) focusSlot(j); }, true);
} catch (e) {} } catch (e) {}
try { readTabScale(id); } catch (e) {}
}); });
panels.appendChild(frame); panels.appendChild(frame);
@@ -479,29 +481,62 @@ if ($effectiveLgIdx) {
} }
document.addEventListener('keydown', handleShortcut); document.addEventListener('keydown', handleShortcut);
// 글씨 크기 조절 — 각 탭(iframe) 내용에 zoom 적용. localStorage 공유 + storage 이벤트로 새 탭/실시간 반영. // 글씨 크기 조절 — 포커스된 탭(메뉴) 내용에만 적용하고, 계정·메뉴별로 서버에 저장한다.
var FONT_KEY = 'jrj_font_scale'; // (예전엔 모든 탭·셸에 일괄 적용돼 "모든 메뉴가 함께 바뀌는" 문제가 있었음)
function curFontScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; } var FONT_MIN = 70, FONT_MAX = 150;
function setFontScale(s) { var FONT_SAVE_URL = '<?= site_url('bag/pref/font-scale') ?>';
s = Math.min(150, Math.max(70, s)); var FONT_CSRF_NAME = '<?= csrf_token() ?>';
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {} var fontCsrfHash = '<?= csrf_hash() ?>';
var z = s / 100; function menuKeyOf(id) {
var u = (tabs[id] && tabs[id].url) ? tabs[id].url : id;
try { return new URL(u, location.href).pathname.replace(/^\/+/, '').replace(/^index\.php\//, ''); } catch (e) { return String(u); }
}
function updateFontPct() {
var fid = slots[focused];
var s = (fid && tabs[fid] && tabs[fid].scale) ? tabs[fid].scale : 100;
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%'; var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
// 탭(iframe) 내용 }
Object.keys(tabs).forEach(function (k) { try { tabs[k].frame.contentDocument.documentElement.style.zoom = z; } catch (e) {} }); // 새 탭 로드 시: embed 레이아웃이 적용한 저장 배율을 읽어 탭 상태로 보관.
// 좌측 사이드바 + 탭 바 텍스트도 함께 확대. function readTabScale(id) {
['.sidebar', '#wsTabBar'].forEach(function (sel) { var s = 100;
var el = document.querySelector(sel); if (el) el.style.zoom = z; try { var z = parseFloat(tabs[id].frame.contentDocument.documentElement.style.zoom || '1'); if (z > 0) s = Math.round(z * 100); } catch (e) {}
}); if (!(s >= FONT_MIN && s <= FONT_MAX)) s = 100;
// 상단 대메뉴는 font-size 만 키운다(바 높이 48px 고정 → A/A+ 버튼이 안 밀림). if (tabs[id]) tabs[id].scale = s;
var topnav = document.querySelector('.portal-top-nav'); if (slots[focused] === id) updateFontPct();
if (topnav) topnav.style.fontSize = (0.875 * z).toFixed(4) + 'rem'; }
var fontSaveTimer = null, fontSavePending = null;
function flushFontSave() {
if (!fontSavePending) return;
var p = fontSavePending; fontSavePending = null;
var b = new URLSearchParams();
b.set(FONT_CSRF_NAME, fontCsrfHash); b.set('menu_key', p.key); b.set('scale', String(p.scale));
fetch(FONT_SAVE_URL, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: b.toString() })
.then(function (r) { return r.json(); }).then(function (d) { if (d && d.csrf) fontCsrfHash = d.csrf; }).catch(function () {});
}
function applyTabFont(id, s, save) {
if (!tabs[id]) return;
s = Math.min(FONT_MAX, Math.max(FONT_MIN, s));
tabs[id].scale = s;
try { tabs[id].frame.contentDocument.documentElement.style.zoom = (s / 100); } catch (e) {}
if (slots[focused] === id) updateFontPct();
if (save) {
var key = menuKeyOf(id);
try { localStorage.setItem('jrj_font_scale::' + key, String(s)); } catch (e) {}
// 연속 클릭 시 CSRF 토큰 회전 경합 방지 → 디바운스로 마지막 값 1회만 저장
fontSavePending = { key: key, scale: s };
clearTimeout(fontSaveTimer);
fontSaveTimer = setTimeout(flushFontSave, 400);
}
} }
(function () { (function () {
setFontScale(curFontScale()); // 저장된 배율을 셸 메뉴에도 적용(초기 로드)
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus'); var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
if (plus) plus.addEventListener('click', function () { setFontScale(curFontScale() + 10); }); function bump(delta) {
if (minus) minus.addEventListener('click', function () { setFontScale(curFontScale() - 10); }); var fid = slots[focused];
if (!fid || !tabs[fid]) { showToast('먼저 메뉴 탭을 여세요.'); return; }
applyTabFont(fid, (tabs[fid].scale || 100) + delta, true);
}
if (plus) plus.addEventListener('click', function () { bump(10); });
if (minus) minus.addEventListener('click', function () { bump(-10); });
})(); })();
// 좌측 사이드바 소메뉴 클릭 → 포커스 칸에 열기 // 좌측 사이드바 소메뉴 클릭 → 포커스 칸에 열기

View File

@@ -0,0 +1,15 @@
-- 계정별 · 메뉴별 글자크기(zoom %) 저장
-- 실행: mysql --default-character-set=utf8mb4 -u USER -p DBNAME < writable/database/member_menu_font_scale.sql
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `member_menu_font_scale` (
`mmf_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`mmf_mb_idx` INT UNSIGNED NOT NULL,
`mmf_menu_key` VARCHAR(191) NOT NULL,
`mmf_scale` SMALLINT UNSIGNED NOT NULL DEFAULT 100,
`mmf_updated` DATETIME NOT NULL,
PRIMARY KEY (`mmf_idx`),
UNIQUE KEY `uk_mb_menu` (`mmf_mb_idx`, `mmf_menu_key`),
KEY `idx_mb` (`mmf_mb_idx`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;