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:
@@ -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>
|
||||
|
||||
@@ -175,12 +175,13 @@ tailwind.config = {
|
||||
document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; });
|
||||
})();
|
||||
|
||||
// 글씨 크기(zoom) — 상단바에서 조절한 값을 적용. localStorage 공유 + storage 이벤트로 실시간 반영.
|
||||
function applyFontScale() {
|
||||
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) {}
|
||||
}
|
||||
applyFontScale();
|
||||
window.addEventListener('storage', function (e) { if (e.key === 'jrj_font_scale') applyFontScale(); });
|
||||
// 글씨 크기(zoom) — 계정별·메뉴별 저장값(서버 주입)을 이 화면에 적용.
|
||||
<?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 s = <?= (int) $__fsc ?>;
|
||||
if (!(s >= 70 && s <= 150)) s = 100;
|
||||
try { document.documentElement.style.zoom = (s / 100); } catch (e) {}
|
||||
})();
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var a = e.target.closest ? e.target.closest('a[href]') : null;
|
||||
|
||||
@@ -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('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; });
|
||||
|
||||
// 글씨 크기 조절 — 본문 + 좌측 사이드바에 zoom 적용.
|
||||
// 상단 헤더는 제외 → A−/A+ 버튼이 커지거나 밀리지 않아 연속 클릭이 편하다.
|
||||
var FONT_KEY = 'jrj_font_scale';
|
||||
// 글씨 크기 조절 — 계정별·메뉴별 저장(서버). 현재 화면 경로 기준으로 개별 적용.
|
||||
<?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 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); });
|
||||
if (plus) plus.addEventListener('click', function () { applyScale(scale + 10, true); });
|
||||
if (minus) minus.addEventListener('click', function () { applyScale(scale - 10, true); });
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
|
||||
@@ -331,7 +331,7 @@ if ($effectiveLgIdx) {
|
||||
persist();
|
||||
}
|
||||
|
||||
function focusSlot(i) { focused = i; render(); }
|
||||
function focusSlot(i) { focused = i; render(); try { updateFontPct(); } catch (e) {} }
|
||||
|
||||
// 포커스 칸에 탭 배치 (이미 다른 칸에 있으면 두 칸을 맞바꿈)
|
||||
function placeInFocused(id) {
|
||||
@@ -343,6 +343,7 @@ if ($effectiveLgIdx) {
|
||||
else { slots[focused] = id; }
|
||||
}
|
||||
render();
|
||||
try { updateFontPct(); } catch (e) {}
|
||||
}
|
||||
|
||||
function setLayout(mode) {
|
||||
@@ -423,6 +424,7 @@ if ($effectiveLgIdx) {
|
||||
d.addEventListener('keydown', handleShortcut);
|
||||
d.addEventListener('mousedown', function () { var j = slots.indexOf(id); if (j >= 0 && j !== focused) focusSlot(j); }, true);
|
||||
} catch (e) {}
|
||||
try { readTabScale(id); } catch (e) {}
|
||||
});
|
||||
panels.appendChild(frame);
|
||||
|
||||
@@ -479,29 +481,62 @@ if ($effectiveLgIdx) {
|
||||
}
|
||||
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; }
|
||||
function setFontScale(s) {
|
||||
s = Math.min(150, Math.max(70, s));
|
||||
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {}
|
||||
var z = s / 100;
|
||||
// 글씨 크기 조절 — 포커스된 탭(메뉴) 내용에만 적용하고, 계정·메뉴별로 서버에 저장한다.
|
||||
// (예전엔 모든 탭·셸에 일괄 적용돼 "모든 메뉴가 함께 바뀌는" 문제가 있었음)
|
||||
var FONT_MIN = 70, FONT_MAX = 150;
|
||||
var FONT_SAVE_URL = '<?= site_url('bag/pref/font-scale') ?>';
|
||||
var FONT_CSRF_NAME = '<?= csrf_token() ?>';
|
||||
var fontCsrfHash = '<?= csrf_hash() ?>';
|
||||
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 + '%';
|
||||
// 탭(iframe) 내용
|
||||
Object.keys(tabs).forEach(function (k) { try { tabs[k].frame.contentDocument.documentElement.style.zoom = z; } catch (e) {} });
|
||||
// 좌측 사이드바 + 탭 바 텍스트도 함께 확대.
|
||||
['.sidebar', '#wsTabBar'].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';
|
||||
}
|
||||
// 새 탭 로드 시: embed 레이아웃이 적용한 저장 배율을 읽어 탭 상태로 보관.
|
||||
function readTabScale(id) {
|
||||
var s = 100;
|
||||
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;
|
||||
if (tabs[id]) tabs[id].scale = s;
|
||||
if (slots[focused] === id) updateFontPct();
|
||||
}
|
||||
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 () {
|
||||
setFontScale(curFontScale()); // 저장된 배율을 셸 메뉴에도 적용(초기 로드)
|
||||
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
|
||||
if (plus) plus.addEventListener('click', function () { setFontScale(curFontScale() + 10); });
|
||||
if (minus) minus.addEventListener('click', function () { setFontScale(curFontScale() - 10); });
|
||||
function bump(delta) {
|
||||
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); });
|
||||
})();
|
||||
|
||||
// 좌측 사이드바 소메뉴 클릭 → 포커스 칸에 열기
|
||||
|
||||
Reference in New Issue
Block a user