2 Commits

Author SHA1 Message Date
taekyoungc
215d87f991 feat: 워크스페이스 탭 자동복원 + 주문 입금확인 토글 + 메뉴 개명(전화접수→주문접수)
종량제3 세션 작업.
- 워크스페이스 탭 계정별 저장·자동복원(member_workspace_tabs 자동생성, pref/workspace-tabs)
- 주문접수 관리 입금확인(so_paid) 수동 토글(phoneOrderMarkPaid, order/phone/manage/paid)
- 메뉴 '전화 접수 관리' → '주문접수관리' 개명(피드백 #35) + 시드/개명 SQL

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 08:04:45 +09:00
taekyoungc
dc5c38d242 style: 브랜드 로고 아이콘 SVG 교체 (로그인·헤더·포털·환영 화면)
종량제3 세션 작업. GBLS 브랜드 로고를 새 아이콘 형태로 통일.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:59:06 +09:00
13 changed files with 226 additions and 28 deletions

View File

@@ -65,6 +65,7 @@ $routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void
$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->post('pref/font-scale', 'Bag::saveFontScale'); // 계정별·메뉴별 글자크기 저장
$routes->post('pref/workspace-tabs', 'Bag::saveWorkspaceTabs'); // 계정별 워크스페이스 탭 상태 저장(자동 복원)
}); });
$routes->get('bag/number-lookup', 'Bag::numberLookup'); $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->get('bag/order/phone/manage', 'Bag::phoneOrderManage');
$routes->post('bag/order/phone/manage/update', 'Bag::phoneOrderUpdate'); $routes->post('bag/order/phone/manage/update', 'Bag::phoneOrderUpdate');
$routes->post('bag/order/phone/manage/scan', 'Bag::phoneOrderManageScan'); $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->post('bag/order/phone/manage/cancel/(:num)', 'Bag::phoneOrderCancel/$1');
$routes->get('bag/order/phone/receipt/(:num)', 'Bag::phoneOrderReceipt/$1'); $routes->get('bag/order/phone/receipt/(:num)', 'Bag::phoneOrderReceipt/$1');
$routes->get('bag/order/change', 'Bag::orderChange'); $routes->get('bag/order/change', 'Bag::orderChange');

View File

@@ -274,6 +274,70 @@ SQL);
return $this->response->setJSON(['ok' => true, 'csrf' => csrf_hash()]); 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', '주문 수정 저장이 완료되었습니다.'); 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 응답). * 전화접수 관리 — 봉투코드 스캔으로 즉시 판매·포장 처리(JSON 응답).
* 선택된 주문의 판매소로 판매 기록(bag_sale) + 재고 차감 + 팩코드 sold 전환 + * 선택된 주문의 판매소로 판매 기록(bag_sale) + 재고 차감 + 팩코드 sold 전환 +
@@ -7921,6 +8016,11 @@ SQL;
return $this->response->setJSON(['ok' => false, 'message' => '선택한 주문을 사용할 수 없습니다.', 'csrf' => csrf_hash()]); 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); $scan = $this->resolveDesignatedSaleBarcode($lgIdx, $barcode);
if (! ($scan['ok'] ?? false)) { if (! ($scan['ok'] ?? false)) {
return $this->response->setJSON(array_merge($scan, ['csrf' => csrf_hash()])); return $this->response->setJSON(array_merge($scan, ['csrf' => csrf_hash()]));

View File

@@ -44,7 +44,11 @@ class Home extends BaseController
} }
helper('admin'); 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]);
} }
/** /**

View File

@@ -44,7 +44,7 @@ if (window.top !== window.self) { try { window.top.location.href = <?= json_enco
<header class="bg-navy text-white h-12 flex items-center justify-between px-4 shrink-0 shadow"> <header class="bg-navy text-white h-12 flex items-center justify-between px-4 shrink-0 shadow">
<a href="<?= base_url() ?>" class="flex items-center gap-2 shrink-0 tracking-tight hover:opacity-90" title="GBLS (Garbage Bag Logistics System)"> <a href="<?= base_url() ?>" class="flex items-center gap-2 shrink-0 tracking-tight hover:opacity-90" title="GBLS (Garbage Bag Logistics System)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-white shrink-0" aria-hidden="true" focusable="false"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-white shrink-0" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/> <path fill="currentColor" fill-rule="evenodd" d="M6 7.5 H18 V19.5 A1.5 1.5 0 0 1 16.5 21 H7.5 A1.5 1.5 0 0 1 6 19.5 Z M6 7.5 L8.5 4.2 H15.5 L18 7.5 Z M10.2 5.2 h3.6 a0.5 0.5 0 0 1 0 1 h-3.6 a0.5 0.5 0 0 1 0 -1 z"/>
</svg> </svg>
<span class="leading-none flex flex-col"> <span class="leading-none flex flex-col">
<strong class="text-base font-extrabold tracking-wide">GBLS</strong> <strong class="text-base font-extrabold tracking-wide">GBLS</strong>

View File

@@ -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) => { referenceRows.forEach((row) => {
row.addEventListener('click', (event) => { row.addEventListener('click', (event) => {
const button = event.target.closest('.js-toggle-bag'); const button = event.target.closest('.js-toggle-bag');

View File

@@ -191,9 +191,12 @@ if ($effectiveLgIdx) {
var STORE_KEY = 'jrj_ws_tabs'; var STORE_KEY = 'jrj_ws_tabs';
var WS_OWNER = '<?= (string) (session()->get('mb_idx') ?? '') ?>'; // 탭 저장 소유자(로그인 사용자) 식별 var WS_OWNER = '<?= (string) (session()->get('mb_idx') ?? '') ?>'; // 탭 저장 소유자(로그인 사용자) 식별
function persist() { var WS_SAVE_URL = '<?= site_url('bag/pref/workspace-tabs') ?>';
try { var WS_CSRF_NAME = '<?= csrf_token() ?>';
sessionStorage.setItem(STORE_KEY, JSON.stringify({ var wsCsrfHash = '<?= csrf_hash() ?>';
var wsSaveTimer = null;
function wsStateObj() {
return {
owner: WS_OWNER, owner: WS_OWNER,
tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }), tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }),
layout: layout, layout: layout,
@@ -201,8 +204,20 @@ if ($effectiveLgIdx) {
vRatio: vRatio, vRatio: vRatio,
hRatio: hRatio, hRatio: hRatio,
slots: slots.map(function (id) { return (id && tabs[id]) ? tabs[id].url : null; }) slots: slots.map(function (id) { return (id && tabs[id]) ? tabs[id].url : null; })
})); };
} catch (e) {} }
function persist() {
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개 미리 생성 // 분할 칸 헤더·빈칸 안내 요소 4개 미리 생성
@@ -560,11 +575,19 @@ if ($effectiveLgIdx) {
}); });
}); });
// 첫 화면: 세션 저장 복원(레이아웃·분할 배치 포함), 없으면 대시보드 1분할 // 첫 화면: ① 계정(DB) 저장 자동복원 → ② 세션 저장 → ③ 없으면 대시보드 1분할
var SERVER_STATE = <?= $savedWorkspaceJson ?? 'null' ?>;
(function restore() { (function restore() {
var saved = null; var 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) {} 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.owner !== WS_OWNER) { try { sessionStorage.removeItem(STORE_KEY); } catch (e) {} saved = null; }
}
if (saved && saved.tabs && saved.tabs.length) { if (saved && saved.tabs && saved.tabs.length) {
saved.tabs.forEach(function (t) { if (t && t.url) openTab(t.url, t.title, { noFocus: true }); }); 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'; layout = (saved.layout === 'lr' || saved.layout === 'tb' || saved.layout === 'quad') ? saved.layout : 'single';

View File

@@ -187,7 +187,7 @@ unset($g);
<th class="text-right">금액</th> <th class="text-right">금액</th>
<th class="text-right">수량</th> <th class="text-right">수량</th>
<th class="text-right">금액</th> <th class="text-right">금액</th>
<th class="text-right">판매수량</th> <th class="text-right">판매수량(팩)</th>
<th class="text-right">금액</th> <th class="text-right">금액</th>
<th class="text-center">B</th> <th class="text-center">B</th>
<th class="text-center">P</th> <th class="text-center">P</th>
@@ -212,7 +212,7 @@ unset($g);
<td class="text-right px-2"><?= number_format($r['box']) ?></td> <td class="text-right px-2"><?= number_format($r['box']) ?></td>
<td class="text-right px-2"><?= number_format($boxPrice) ?></td> <td class="text-right px-2"><?= number_format($boxPrice) ?></td>
<td class="text-right px-2"><?= number_format($r['price']) ?></td> <td class="text-right px-2"><?= number_format($r['price']) ?></td>
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input" name="item_qty[]" type="number" min="0" value="0"/></td> <td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input item-pack-input" type="number" min="0" value="0"/><input type="hidden" name="item_qty[]" class="item-qty-hidden" value="0"/></td>
<td class="text-right px-2 item-amount-cell">0</td> <td class="text-right px-2 item-amount-cell">0</td>
<td class="text-center item-box-cell">0</td> <td class="text-center item-box-cell">0</td>
<td class="text-center item-pack-cell">0</td> <td class="text-center item-pack-cell">0</td>
@@ -330,12 +330,18 @@ unset($g);
} }
function calcRow(row) { function calcRow(row) {
const qtyInput = row.querySelector('.item-qty-input'); const qtyInput = row.querySelector('.item-pack-input');
const qty = parseInt(qtyInput.value || '0', 10) || 0; const hiddenQty = row.querySelector('.item-qty-hidden');
const packQty = parseInt(qtyInput.value || '0', 10) || 0; // 입력값 = 팩 수
const unitPrice = parseInt(row.dataset.unitPrice || '0', 10) || 0; const unitPrice = parseInt(row.dataset.unitPrice || '0', 10) || 0;
const boxSheets = parseInt(row.dataset.boxSheets || '0', 10) || 0; const boxSheets = parseInt(row.dataset.boxSheets || '0', 10) || 0;
const packSheets = parseInt(row.dataset.packSheets || '0', 10) || 0; const packSheets = parseInt(row.dataset.packSheets || '0', 10) || 0;
// 팩당 낱장 수(팩 매수 미설정 봉투는 1:1로 처리)
const mult = packSheets > 0 ? packSheets : 1;
const qty = packQty * mult; // 실제 낱장 수 → 서버로 전송
if (hiddenQty) hiddenQty.value = String(qty);
let box = 0; let box = 0;
let pack = 0; let pack = 0;
let sheet = qty; let sheet = qty;
@@ -359,14 +365,14 @@ unset($g);
row.querySelector('.item-pack-cell').textContent = pack ? nf(pack) : ''; row.querySelector('.item-pack-cell').textContent = pack ? nf(pack) : '';
row.querySelector('.item-sheet-cell').textContent = sheet ? nf(sheet) : ''; row.querySelector('.item-sheet-cell').textContent = sheet ? nf(sheet) : '';
return { qty, amount, box, pack, sheet }; return { packQty, amount, box, pack, sheet };
} }
function recalcAllRows() { function recalcAllRows() {
let sumQty = 0, sumAmount = 0, sumBox = 0, sumPack = 0, sumSheet = 0; let sumQty = 0, sumAmount = 0, sumBox = 0, sumPack = 0, sumSheet = 0;
document.querySelectorAll('.order-row').forEach((row) => { document.querySelectorAll('.order-row').forEach((row) => {
const r = calcRow(row); const r = calcRow(row);
sumQty += r.qty; sumQty += r.packQty;
sumAmount += r.amount; sumAmount += r.amount;
sumBox += r.box; sumBox += r.box;
sumPack += r.pack; sumPack += r.pack;

View File

@@ -69,12 +69,14 @@
<input type="date" lang="ko" name="so_delivery_date" id="detail-delivery-date-input" class="border border-gray-300 rounded px-2 py-0.5 text-sm"/> <input type="date" lang="ko" name="so_delivery_date" id="detail-delivery-date-input" class="border border-gray-300 rounded px-2 py-0.5 text-sm"/>
</div> </div>
<div><span class="font-semibold text-gray-700 inline-block w-20">상태</span> <span id="detail-status">-</span></div> <div><span class="font-semibold text-gray-700 inline-block w-20">상태</span> <span id="detail-status">-</span></div>
<div><span class="font-semibold text-gray-700 inline-block w-20">입금상태</span> <span id="detail-paid-status" class="font-semibold">-</span></div>
</div> </div>
</div> </div>
<div class="flex flex-wrap items-center justify-between gap-2 px-1"> <div class="flex flex-wrap items-center justify-between gap-2 px-1">
<span class="text-sm font-semibold text-gray-700">접수 품목 내역</span> <span class="text-sm font-semibold text-gray-700">접수 품목 내역</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" id="btn-toggle-paid" class="border border-emerald-300 text-emerald-700 px-3 py-1 rounded-sm text-sm hover:bg-emerald-50" disabled>입금확인</button>
<button type="submit" id="btn-save" class="bg-btn-search text-white px-3 py-1 rounded-sm text-sm shadow hover:opacity-90" disabled>주문 수정 저장</button> <button type="submit" id="btn-save" class="bg-btn-search text-white px-3 py-1 rounded-sm text-sm shadow hover:opacity-90" disabled>주문 수정 저장</button>
<button type="button" id="btn-receipt" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50" disabled>입금자 영수증 출력</button> <button type="button" id="btn-receipt" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50" disabled>입금자 영수증 출력</button>
<button type="submit" id="btn-cancel-order" form="order-cancel-form" class="border border-red-300 text-red-600 px-3 py-1 rounded-sm text-sm hover:bg-red-50" disabled>주문 취소</button> <button type="submit" id="btn-cancel-order" form="order-cancel-form" class="border border-red-300 text-red-600 px-3 py-1 rounded-sm text-sm hover:bg-red-50" disabled>주문 취소</button>
@@ -238,6 +240,8 @@
const btnSave = document.getElementById('btn-save'); const btnSave = document.getElementById('btn-save');
const btnCancel = document.getElementById('btn-cancel-order'); const btnCancel = document.getElementById('btn-cancel-order');
const btnReceipt = document.getElementById('btn-receipt'); const btnReceipt = document.getElementById('btn-receipt');
const btnTogglePaid = document.getElementById('btn-toggle-paid');
const paidApi = new URL('<?= base_url('bag/order/phone/manage/paid') ?>', window.location.href).pathname;
const receiptBase = '<?= base_url('bag/order/phone/receipt') ?>'; const receiptBase = '<?= base_url('bag/order/phone/receipt') ?>';
btnReceipt?.addEventListener('click', () => { btnReceipt?.addEventListener('click', () => {
const id = inputSoIdx.value; const id = inputSoIdx.value;
@@ -252,6 +256,43 @@
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0); const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
function paintPaidButton(order) {
if (!btnTogglePaid) return;
const paid = order && Number(order.so_paid) === 1;
btnTogglePaid.textContent = paid ? '입금확인 취소' : '입금확인';
btnTogglePaid.className = paid
? 'border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50'
: 'border border-emerald-300 text-emerald-700 px-3 py-1 rounded-sm text-sm hover:bg-emerald-50';
}
btnTogglePaid?.addEventListener('click', async () => {
const id = inputSoIdx.value;
if (!id) return;
const o = orderMap.get(String(id));
if (!o) return;
const nextPaid = Number(o.so_paid) === 1 ? 0 : 1;
if (nextPaid === 0 && !confirm('입금확인을 취소하면 이 주문은 포장(스캔)이 차단됩니다.\n계속할까요?')) return;
const body = new URLSearchParams();
body.set(csrfName, csrfHash);
body.set('so_idx', String(id));
body.set('paid', String(nextPaid));
btnTogglePaid.disabled = true;
try {
const res = await fetch(paidApi, { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body });
const data = await res.json();
if (data && data.csrf) csrfHash = data.csrf;
if (!data || !data.ok) { alert((data && data.message) || '처리에 실패했습니다.'); btnTogglePaid.disabled = false; return; }
o.so_paid = nextPaid;
setHeader(o);
paintPaidButton(o);
btnTogglePaid.disabled = false;
renderList(); // 리스트의 입금 표시 갱신
} catch (e) {
alert('네트워크 오류로 처리하지 못했습니다.');
btnTogglePaid.disabled = false;
}
});
function setHeader(order) { function setHeader(order) {
var t = function (id, v) { document.getElementById(id).textContent = (v === undefined || v === null || v === '') ? '-' : v; }; var t = function (id, v) { document.getElementById(id).textContent = (v === undefined || v === null || v === '') ? '-' : v; };
t('detail-so-no', order ? displayNo(order) : ''); t('detail-so-no', order ? displayNo(order) : '');
@@ -263,6 +304,12 @@
t('detail-shop-addr', order ? order.ds_addr : ''); t('detail-shop-addr', order ? order.ds_addr : '');
t('detail-order-date', order ? order.so_order_date : ''); t('detail-order-date', order ? order.so_order_date : '');
t('detail-status', order ? ((order.so_status === 'cancelled') ? '취소' : '정상') : ''); t('detail-status', order ? ((order.so_status === 'cancelled') ? '취소' : '정상') : '');
var ps = document.getElementById('detail-paid-status');
if (ps) {
var paid = order && Number(order.so_paid) === 1;
ps.textContent = order ? (paid ? '입금완료' : '미입금') : '-';
ps.style.color = order ? (paid ? '#059669' : '#e11d48') : '';
}
// 배달일 — 수정 가능한 date 입력 // 배달일 — 수정 가능한 date 입력
var dd = document.getElementById('detail-delivery-date-input'); var dd = document.getElementById('detail-delivery-date-input');
if (dd) { if (dd) {
@@ -346,6 +393,7 @@
btnSave.disabled = isCancelled; btnSave.disabled = isCancelled;
btnCancel.disabled = isCancelled; btnCancel.disabled = isCancelled;
if (btnReceipt) btnReceipt.disabled = false; if (btnReceipt) btnReceipt.disabled = false;
if (btnTogglePaid) { btnTogglePaid.disabled = isCancelled; paintPaidButton(order); }
renderPacking(order); // 포장 명세 탭 동기화 renderPacking(order); // 포장 명세 탭 동기화

View File

@@ -8,7 +8,7 @@ $linkClass = $linkClass ?? 'app-brand flex items-center gap-2 shrink-0 text-base
?> ?>
<a href="<?= esc($href) ?>" class="<?= esc($linkClass, 'attr') ?>" title="GBLS (Garbage Bag Logistics System)"> <a href="<?= esc($href) ?>" class="<?= esc($linkClass, 'attr') ?>" title="GBLS (Garbage Bag Logistics System)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-blue-900 translate-y-[1px] shrink-0" aria-hidden="true" focusable="false"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-blue-900 translate-y-[1px] shrink-0" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/> <path fill="currentColor" fill-rule="evenodd" d="M6 7.5 H18 V19.5 A1.5 1.5 0 0 1 16.5 21 H7.5 A1.5 1.5 0 0 1 6 19.5 Z M6 7.5 L8.5 4.2 H15.5 L18 7.5 Z M10.2 5.2 h3.6 a0.5 0.5 0 0 1 0 1 h-3.6 a0.5 0.5 0 0 1 0 -1 z"/>
</svg> </svg>
<span class="leading-none flex flex-col"> <span class="leading-none flex flex-col">
<strong class="font-extrabold tracking-wide">GBLS</strong> <strong class="font-extrabold tracking-wide">GBLS</strong>

View File

@@ -9,7 +9,7 @@ $brandHref = $brandHref ?? base_url('dashboard/gov-portal');
?> ?>
<a href="<?= esc($brandHref) ?>" class="gov-portal-brand"> <a href="<?= esc($brandHref) ?>" class="gov-portal-brand">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/> <path fill="currentColor" fill-rule="evenodd" d="M6 7.5 H18 V19.5 A1.5 1.5 0 0 1 16.5 21 H7.5 A1.5 1.5 0 0 1 6 19.5 Z M6 7.5 L8.5 4.2 H15.5 L18 7.5 Z M10.2 5.2 h3.6 a0.5 0.5 0 0 1 0 1 h-3.6 a0.5 0.5 0 0 1 0 -1 z"/>
</svg> </svg>
<span style="display:inline-flex;flex-direction:column;line-height:1.02;"> <span style="display:inline-flex;flex-direction:column;line-height:1.02;">
<strong style="font-size:1.02rem;font-weight:800;letter-spacing:.5px;">GBLS</strong> <strong style="font-size:1.02rem;font-weight:800;letter-spacing:.5px;">GBLS</strong>

View File

@@ -28,7 +28,7 @@ tailwind.config = {
<header class="bg-navy text-white h-12 flex items-center justify-between px-4 shrink-0 shadow"> <header class="bg-navy text-white h-12 flex items-center justify-between px-4 shrink-0 shadow">
<a href="<?= base_url() ?>" class="flex items-center gap-2 tracking-tight hover:opacity-90" title="GBLS (Garbage Bag Logistics System)"> <a href="<?= base_url() ?>" class="flex items-center gap-2 tracking-tight hover:opacity-90" title="GBLS (Garbage Bag Logistics System)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-white shrink-0" aria-hidden="true" focusable="false"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-white shrink-0" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/> <path fill="currentColor" fill-rule="evenodd" d="M6 7.5 H18 V19.5 A1.5 1.5 0 0 1 16.5 21 H7.5 A1.5 1.5 0 0 1 6 19.5 Z M6 7.5 L8.5 4.2 H15.5 L18 7.5 Z M10.2 5.2 h3.6 a0.5 0.5 0 0 1 0 1 h-3.6 a0.5 0.5 0 0 1 0 -1 z"/>
</svg> </svg>
<span class="leading-none flex flex-col"> <span class="leading-none flex flex-col">
<strong class="text-base font-extrabold tracking-wide">GBLS</strong> <strong class="text-base font-extrabold tracking-wide">GBLS</strong>
@@ -46,7 +46,7 @@ tailwind.config = {
<div class="bg-gradient-to-br from-navy to-[#007bff] text-white px-8 py-8 text-center"> <div class="bg-gradient-to-br from-navy to-[#007bff] text-white px-8 py-8 text-center">
<div class="inline-flex h-14 w-14 items-center justify-center rounded-full bg-white/15 mb-3"> <div class="inline-flex h-14 w-14 items-center justify-center rounded-full bg-white/15 mb-3">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-7 w-7 text-white" aria-hidden="true" focusable="false"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-7 w-7 text-white" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/> <path fill="currentColor" fill-rule="evenodd" d="M6 7.5 H18 V19.5 A1.5 1.5 0 0 1 16.5 21 H7.5 A1.5 1.5 0 0 1 6 19.5 Z M6 7.5 L8.5 4.2 H15.5 L18 7.5 Z M10.2 5.2 h3.6 a0.5 0.5 0 0 1 0 1 h-3.6 a0.5 0.5 0 0 1 0 -1 z"/>
</svg> </svg>
</div> </div>
<h1 class="text-xl font-bold">종량제 쓰레기봉투 물류시스템</h1> <h1 class="text-xl font-bold">종량제 쓰레기봉투 물류시스템</h1>

View File

@@ -0,0 +1,5 @@
-- 사용자 의견 #35: 메뉴 '전화 접수 관리' → '주문접수관리' 개명
-- 실행: mysql -u jongryangje -p jongryangje_dev < writable/database/menu_rename_phone_to_order.sql
-- 멱등(이미 바뀌었으면 0건).
UPDATE `menu` SET `mm_name` = '주문접수관리' WHERE `mm_name` = '전화 접수 관리';

View File

@@ -177,7 +177,7 @@ INSERT INTO menu (mt_idx, lg_idx, mm_name, mm_link, mm_pidx, mm_dep, mm_num, mm_
SELECT @mt_site, 1, t.mm_name, SELECT @mt_site, 1, t.mm_name,
CASE t.mm_name CASE t.mm_name
WHEN '전화 접수' THEN 'bag/shop-orders' WHEN '전화 접수' THEN 'bag/shop-orders'
WHEN '전화 접수 관리' THEN 'bag/shop-orders' WHEN '주문접수관리' THEN 'bag/shop-orders'
WHEN '지정 판매소 판매' THEN 'bag/sale/create' WHEN '지정 판매소 판매' THEN 'bag/sale/create'
WHEN '지정 판매소 반품' THEN 'bag/bag-sales' WHEN '지정 판매소 반품' THEN 'bag/bag-sales'
WHEN '지정 판매소 판매 취소' THEN 'bag/bag-sales' WHEN '지정 판매소 판매 취소' THEN 'bag/bag-sales'
@@ -187,7 +187,7 @@ SELECT @mt_site, 1, t.mm_name,
@parent_sales, 1, t.mm_num, 0, '', 'Y' @parent_sales, 1, t.mm_num, 0, '', 'Y'
FROM ( FROM (
SELECT 0 AS mm_num, '전화 접수' AS mm_name UNION ALL SELECT 0 AS mm_num, '전화 접수' AS mm_name UNION ALL
SELECT 1, '전화 접수 관리' UNION ALL SELECT 1, '주문접수관리' UNION ALL
SELECT 2, '지정 판매소 판매' UNION ALL SELECT 2, '지정 판매소 판매' UNION ALL
SELECT 3, '지정 판매소 반품' UNION ALL SELECT 3, '지정 판매소 반품' UNION ALL
SELECT 4, '지정 판매소 판매 취소' UNION ALL SELECT 4, '지정 판매소 판매 취소' UNION ALL