From 03c2bbc05c010e65861fc42b3703a977c582dd7b Mon Sep 17 00:00:00 2001 From: taekyoungc Date: Mon, 13 Jul 2026 11:12:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B8=80=EC=9E=90=ED=81=AC=EA=B8=B0?= =?UTF-8?q?=EB=A5=BC=20=EA=B3=84=EC=A0=95=EB=B3=84=C2=B7=EB=A9=94=EB=89=B4?= =?UTF-8?q?=EB=B3=84=EB=A1=9C=20=EC=A0=80=EC=9E=A5(=EC=9E=AC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 전역 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 --- app/Config/Routes.php | 1 + app/Controllers/Bag.php | 50 +++++++++++++ app/Helpers/admin_helper.php | 31 ++++++++ app/Views/admin/layout.php | 38 +++++++--- app/Views/bag/layout/embed.php | 13 ++-- app/Views/bag/layout/portal.php | 34 ++++++--- app/Views/bag/layout/workspace.php | 75 ++++++++++++++------ writable/database/member_menu_font_scale.sql | 15 ++++ 8 files changed, 210 insertions(+), 47 deletions(-) create mode 100644 writable/database/member_menu_font_scale.sql diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e409ade..e06d0f5 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -63,6 +63,7 @@ $routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void $routes->get('manual/search', 'Bag::manualSearch'); // (:segment) 보다 먼저 $routes->get('manual/(:segment)', 'Bag::manualPage/$1'); $routes->post('activity/print-log', 'Bag::printLog'); // 인쇄 활동 기록(beacon) + $routes->post('pref/font-scale', 'Bag::saveFontScale'); // 계정별·메뉴별 글자크기 저장 }); $routes->get('bag/number-lookup', 'Bag::numberLookup'); diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php index 14fb119..440ff7e 100644 --- a/app/Controllers/Bag.php +++ b/app/Controllers/Bag.php @@ -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()]); + } + // ────────────────────────────────────────────── // 기본정보관리 (단가·포장 단위 진입 허브) // ────────────────────────────────────────────── diff --git a/app/Helpers/admin_helper.php b/app/Helpers/admin_helper.php index c01f69a..e576d9e 100644 --- a/app/Helpers/admin_helper.php +++ b/app/Helpers/admin_helper.php @@ -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')) { /** * menu.mm_link 를 base_url() 인자로 쓸 수 있는 상대 경로로 정규화합니다. diff --git a/app/Views/admin/layout.php b/app/Views/admin/layout.php index 54fc564..39498e3 100644 --- a/app/Views/admin/layout.php +++ b/app/Views/admin/layout.php @@ -62,6 +62,9 @@ $navPartial = [ <?= esc($title ?? '관리자') ?> - GBLS + + + @@ -174,27 +177,40 @@ tailwind.config = { window.addEventListener('pagehide', closeStuckOverlays); })(); - // 글씨 크기 조절(A−/A+) — 본문 + 상단 대메뉴 + 좌측 사이드바에 zoom 적용. 사이트/워크스페이스와 배율 공유. + // 글씨 크기 조절(A−/A+) — 계정별·메뉴별 저장(서버). 현재 화면 경로 기준으로 개별 적용. + (function () { - var FONT_KEY = 'jrj_font_scale'; - // 상단 헤더는 제외 → A−/A+ 버튼이 커지거나 밀리지 않아 연속 클릭이 편하다. + var MENU_KEY = ; + var scale = ; + var SAVE_URL = ; + var CSRF_NAME = ; + var csrfHash = ; 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); }); })();