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

@@ -62,6 +62,9 @@ $navPartial = [
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<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 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>
@@ -174,27 +177,40 @@ tailwind.config = {
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 () {
var FONT_KEY = 'jrj_font_scale';
// 상단 헤더는 제외 → A/A+ 버튼이 커지거나 밀리지 않아 연속 클릭이 편하다.
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'];
function curScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; }
function applyScale(s) {
var saveTimer = null;
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));
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {}
scale = s;
var z = s / 100;
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');
if (topnav) topnav.style.fontSize = (0.875 * z).toFixed(4) + 'rem';
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');
if (plus) plus.addEventListener('click', function () { applyScale(curScale() + 10); });
if (minus) minus.addEventListener('click', function () { applyScale(curScale() - 10); });
window.addEventListener('storage', function (e) { if (e.key === FONT_KEY) applyScale(curScale()); });
if (plus) plus.addEventListener('click', function () { applyScale(scale + 10, true); });
if (minus) minus.addEventListener('click', function () { applyScale(scale - 10, true); });
})();
</script>
<script>