diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 3c2cda9..e044d58 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -65,6 +65,7 @@ $routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void $routes->get('manual/(:segment)', 'Bag::manualPage/$1'); $routes->post('activity/print-log', 'Bag::printLog'); // 인쇄 활동 기록(beacon) $routes->post('pref/font-scale', 'Bag::saveFontScale'); // 계정별·메뉴별 글자크기 저장 + $routes->post('pref/workspace-tabs', 'Bag::saveWorkspaceTabs'); // 계정별 워크스페이스 탭 상태 저장(자동 복원) }); $routes->get('bag/number-lookup', 'Bag::numberLookup'); @@ -81,6 +82,7 @@ $routes->get('bag/order/phone', 'Bag::phoneOrderCreate'); $routes->get('bag/order/phone/manage', 'Bag::phoneOrderManage'); $routes->post('bag/order/phone/manage/update', 'Bag::phoneOrderUpdate'); $routes->post('bag/order/phone/manage/scan', 'Bag::phoneOrderManageScan'); +$routes->post('bag/order/phone/manage/paid', 'Bag::phoneOrderMarkPaid'); $routes->post('bag/order/phone/manage/cancel/(:num)', 'Bag::phoneOrderCancel/$1'); $routes->get('bag/order/phone/receipt/(:num)', 'Bag::phoneOrderReceipt/$1'); $routes->get('bag/order/change', 'Bag::orderChange'); diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php index 9f6b1d9..03ad219 100644 --- a/app/Controllers/Bag.php +++ b/app/Controllers/Bag.php @@ -274,6 +274,70 @@ SQL); return $this->response->setJSON(['ok' => true, 'csrf' => csrf_hash()]); } + /** member_workspace_tabs 테이블 보장(없으면 생성) */ + private function ensureWorkspaceTabsTable(\CodeIgniter\Database\BaseConnection $db): void + { + if ($db->tableExists('member_workspace_tabs')) { + return; + } + $db->query(<<<'SQL' +CREATE TABLE IF NOT EXISTS `member_workspace_tabs` ( + `mwt_mb_idx` INT UNSIGNED NOT NULL, + `mwt_state` MEDIUMTEXT NULL, + `mwt_updated` DATETIME NOT NULL, + PRIMARY KEY (`mwt_mb_idx`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +SQL); + } + + /** 계정별 워크스페이스 탭 상태 조회(자동 복원용). 없으면 'null'. */ + public function loadWorkspaceTabsState(int $mbIdx): string + { + if ($mbIdx <= 0) { + return 'null'; + } + $db = \Config\Database::connect(); + if (! $db->tableExists('member_workspace_tabs')) { + return 'null'; + } + $row = $db->table('member_workspace_tabs')->select('mwt_state')->where('mwt_mb_idx', $mbIdx)->get()->getRowArray(); + $s = (string) ($row['mwt_state'] ?? ''); + return $s !== '' ? $s : 'null'; + } + + /** 계정별 워크스페이스 탭 상태 저장 (자동 복원) */ + public function saveWorkspaceTabs(): ResponseInterface + { + $mbIdx = (int) (session()->get('mb_idx') ?? 0); + if ($mbIdx <= 0) { + return $this->response->setJSON(['ok' => false, 'csrf' => csrf_hash()]); + } + $state = (string) ($this->request->getPost('state') ?? ''); + // 크기 제한 + JSON 유효성 + if (strlen($state) > 30000) { + return $this->response->setJSON(['ok' => false, 'csrf' => csrf_hash()]); + } + if ($state !== '') { + json_decode($state); + if (json_last_error() !== JSON_ERROR_NONE) { + return $this->response->setJSON(['ok' => false, 'csrf' => csrf_hash()]); + } + } + + $db = \Config\Database::connect(); + $this->ensureWorkspaceTabsTable($db); + $now = date('Y-m-d H:i:s'); + $exists = $db->table('member_workspace_tabs')->where('mwt_mb_idx', $mbIdx)->countAllResults(); + if ($exists > 0) { + $db->table('member_workspace_tabs')->where('mwt_mb_idx', $mbIdx) + ->update(['mwt_state' => $state, 'mwt_updated' => $now]); + } else { + $db->table('member_workspace_tabs')->insert(['mwt_mb_idx' => $mbIdx, 'mwt_state' => $state, 'mwt_updated' => $now]); + } + + return $this->response->setJSON(['ok' => true, 'csrf' => csrf_hash()]); + } + // ────────────────────────────────────────────── // 기본정보관리 (단가·포장 단위 진입 허브) // ────────────────────────────────────────────── @@ -7896,6 +7960,37 @@ SQL; return redirect()->to(site_url('bag/order/phone/manage'))->with('success', '주문 수정 저장이 완료되었습니다.'); } + /** + * 주문접수 관리 — 입금확인(so_paid) 수동 토글(JSON 응답). + * 입금확인이 완료되어야 포장(스캔)이 가능하다. + */ + public function phoneOrderMarkPaid() + { + $lgIdx = $this->lgIdx(); + if (! $lgIdx) { + return $this->response->setJSON(['ok' => false, 'message' => '지자체를 선택해 주세요.', 'csrf' => csrf_hash()]); + } + + $soIdx = (int) ($this->request->getPost('so_idx') ?? 0); + $paid = ((int) ($this->request->getPost('paid') ?? 0)) === 1 ? 1 : 0; + if ($soIdx <= 0) { + return $this->response->setJSON(['ok' => false, 'message' => '주문 값이 올바르지 않습니다.', 'csrf' => csrf_hash()]); + } + + $orderModel = model(ShopOrderModel::class); + $order = $orderModel->where('so_idx', $soIdx)->where('so_lg_idx', $lgIdx)->first(); + if (! $order) { + return $this->response->setJSON(['ok' => false, 'message' => '선택한 주문을 찾을 수 없습니다.', 'csrf' => csrf_hash()]); + } + if ((string) ($order->so_status ?? '') === 'cancelled') { + return $this->response->setJSON(['ok' => false, 'message' => '취소된 주문은 입금상태를 변경할 수 없습니다.', 'csrf' => csrf_hash()]); + } + + $orderModel->update($soIdx, ['so_paid' => $paid]); + + return $this->response->setJSON(['ok' => true, 'so_idx' => $soIdx, 'paid' => $paid, 'csrf' => csrf_hash()]); + } + /** * 전화접수 관리 — 봉투코드 스캔으로 즉시 판매·포장 처리(JSON 응답). * 선택된 주문의 판매소로 판매 기록(bag_sale) + 재고 차감 + 팩코드 sold 전환 + @@ -7921,6 +8016,11 @@ SQL; return $this->response->setJSON(['ok' => false, 'message' => '선택한 주문을 사용할 수 없습니다.', 'csrf' => csrf_hash()]); } + // 입금확인(so_paid=1)이 안 된 주문은 포장(판매) 스캔 불가. + if ((int) ($order->so_paid ?? 0) !== 1) { + return $this->response->setJSON(['ok' => false, 'message' => '입금확인이 완료되지 않은 주문입니다. 입금확인 후 포장하세요.', 'csrf' => csrf_hash()]); + } + $scan = $this->resolveDesignatedSaleBarcode($lgIdx, $barcode); if (! ($scan['ok'] ?? false)) { return $this->response->setJSON(array_merge($scan, ['csrf' => csrf_hash()])); diff --git a/app/Controllers/Home.php b/app/Controllers/Home.php index 6a19789..57c9706 100644 --- a/app/Controllers/Home.php +++ b/app/Controllers/Home.php @@ -44,7 +44,11 @@ class Home extends BaseController } helper('admin'); - return view('bag/layout/workspace'); + // 계정별 저장된 탭 상태(자동 복원). 없으면 'null'. + $savedWorkspaceJson = (new \App\Controllers\Bag()) + ->loadWorkspaceTabsState((int) (session()->get('mb_idx') ?? 0)); + + return view('bag/layout/workspace', ['savedWorkspaceJson' => $savedWorkspaceJson]); } /** diff --git a/app/Views/bag/create_bag_order.php b/app/Views/bag/create_bag_order.php index 56ba81b..394dd86 100644 --- a/app/Views/bag/create_bag_order.php +++ b/app/Views/bag/create_bag_order.php @@ -788,6 +788,16 @@ } }); + // 박스수량 입력 중 Enter → 다음 봉투의 박스수량 입력칸으로 이동(#39) + selectedBody.addEventListener('keydown', (event) => { + if (event.key !== 'Enter' || !event.target.classList.contains('item-qty-box')) return; + event.preventDefault(); + const inputs = Array.from(selectedBody.querySelectorAll('.item-qty-box')); + const next = inputs[inputs.indexOf(event.target) + 1]; + if (next) { next.focus(); next.select(); } + else { event.target.blur(); } + }); + referenceRows.forEach((row) => { row.addEventListener('click', (event) => { const button = event.target.closest('.js-toggle-bag'); diff --git a/app/Views/bag/layout/workspace.php b/app/Views/bag/layout/workspace.php index 375f33c..468cd0b 100644 --- a/app/Views/bag/layout/workspace.php +++ b/app/Views/bag/layout/workspace.php @@ -191,18 +191,33 @@ if ($effectiveLgIdx) { var STORE_KEY = 'jrj_ws_tabs'; var WS_OWNER = '= (string) (session()->get('mb_idx') ?? '') ?>'; // 탭 저장 소유자(로그인 사용자) 식별 + var WS_SAVE_URL = '= site_url('bag/pref/workspace-tabs') ?>'; + var WS_CSRF_NAME = '= csrf_token() ?>'; + var wsCsrfHash = '= csrf_hash() ?>'; + var wsSaveTimer = null; + function wsStateObj() { + return { + owner: WS_OWNER, + tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }), + layout: layout, + focused: focused, + vRatio: vRatio, + hRatio: hRatio, + slots: slots.map(function (id) { return (id && tabs[id]) ? tabs[id].url : null; }) + }; + } function persist() { - try { - sessionStorage.setItem(STORE_KEY, JSON.stringify({ - owner: WS_OWNER, - tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }), - layout: layout, - focused: focused, - vRatio: vRatio, - hRatio: hRatio, - slots: slots.map(function (id) { return (id && tabs[id]) ? tabs[id].url : null; }) - })); - } catch (e) {} + var obj = wsStateObj(); + var json = JSON.stringify(obj); + try { sessionStorage.setItem(STORE_KEY, json); } catch (e) {} + // 계정(DB)에 저장 → 다음 로그인/다른 기기에서도 자동 복원 (연속 변경은 디바운스) + clearTimeout(wsSaveTimer); + wsSaveTimer = setTimeout(function () { + var b = new URLSearchParams(); + b.set(WS_CSRF_NAME, wsCsrfHash); b.set('state', json); + fetch(WS_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) wsCsrfHash = d.csrf; }).catch(function () {}); + }, 500); } // 분할 칸 헤더·빈칸 안내 요소 4개 미리 생성 @@ -560,11 +575,19 @@ if ($effectiveLgIdx) { }); }); - // 첫 화면: 세션 저장 복원(레이아웃·분할 배치 포함), 없으면 대시보드 1분할 + // 첫 화면: ① 계정(DB) 저장 자동복원 → ② 세션 저장 → ③ 없으면 대시보드 1분할 + var SERVER_STATE = = $savedWorkspaceJson ?? 'null' ?>; (function restore() { var saved = null; - try { saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) {} - if (saved && saved.owner !== WS_OWNER) { try { sessionStorage.removeItem(STORE_KEY); } catch (e) {} saved = null; } + // ① 계정 DB에 저장된 상태 우선(다음 로그인·다른 기기에서도 복원) + if (SERVER_STATE && SERVER_STATE.tabs && SERVER_STATE.tabs.length) { + saved = SERVER_STATE; + } + // ② 없으면 이 브라우저 세션 저장 사용 + if (!saved) { + try { saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) {} + if (saved && saved.owner !== WS_OWNER) { try { sessionStorage.removeItem(STORE_KEY); } catch (e) {} saved = null; } + } if (saved && saved.tabs && saved.tabs.length) { saved.tabs.forEach(function (t) { if (t && t.url) openTab(t.url, t.title, { noFocus: true }); }); layout = (saved.layout === 'lr' || saved.layout === 'tb' || saved.layout === 'quad') ? saved.layout : 'single'; diff --git a/app/Views/bag/order_phone.php b/app/Views/bag/order_phone.php index f9f9342..123c18d 100644 --- a/app/Views/bag/order_phone.php +++ b/app/Views/bag/order_phone.php @@ -187,7 +187,7 @@ unset($g);