Files
jongryangje/app/Views/bag/_dev_all_sales_panel.php
taekyoungc 8763876f19 사용자 매뉴얼·번호알기·gov-portal 대시보드와 메뉴 동선·수불 리포트를 보강한다.
- 사용자 매뉴얼: league/commonmark 기반 bag/manual(로그인 전용),
  ManualRenderer + Config\Manual manifest, 콘텐츠 8종, E2E
- 번호알기(봉투번호확인): bag/number-lookup, BagNumberLookup, E2E
- gov-portal 대시보드 시안(기본/strip)·기본코드관리 화면
- 메뉴 관리: 등록·수정 후 메뉴 화면 유지, 수정 버튼 클릭 시 상단 스크롤
- 수불/분석 리포트(LOT 수불·반품/파기·수급계획·추이) 표시 보강
- .gitignore: docs/ → /docs/ 앵커링(최상위 개발문서만 제외, app/Docs는 추적)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:46:51 +09:00

169 lines
8.1 KiB
PHP

<?php
/**
* [개발용 임시] 판매관리·전화접수 화면 하단에 끼우는 "전체 처리 흐름" 표.
*
* - 통합 데이터 출처:
* · shop_order + shop_order_item (단계: order) — 전화/일반 주문 접수
* · bag_sale_scan_code state=sold (단계: sale) — 지정판매소 판매 처리
* · bag_sale_scan_code state=in_stock (단계: returned) — 반품으로 재고 복귀
* - 호출 API: GET /bag/sale/dev-all-sales-history
* - 운영 배포 시 본 파일과 라우트/컨트롤러(`Bag::devAllSalesHistory`) 함께 제거.
*
* 다수 페이지에 동일하게 include 되므로 ID 충돌 회피를 위해 dev-all-sales- 접두어 사용.
*/
?>
<section id="dev-all-sales-panel" class="border border-amber-400 bg-amber-50/50 p-3 mt-3 rounded-sm">
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<p class="text-xs text-amber-900 leading-relaxed m-0">
<strong class="text-amber-950">[개발용 임시 — 전체 처리 흐름]</strong>
현재 지자체의 <strong>주문 접수(order) · 판매 처리(sale) · 반품 복귀(returned)</strong>를 모두 모아
<strong>일시 역순 최근 500건</strong>으로 표시합니다. (일시는 DB UTC 기준을 <strong>한국 표준시(KST)</strong>로 변환)
운영 배포 시 이 블록을 제거해 주세요.
</p>
<div class="flex items-center gap-2">
<span id="dev-all-sales-count" class="text-[13px] text-amber-900"></span>
<button type="button" id="dev-all-sales-refresh" class="text-[13px] border border-amber-500 text-amber-800 bg-white rounded px-2 py-0.5 hover:bg-amber-100">새로고침</button>
</div>
</div>
<div class="max-h-80 overflow-auto border border-amber-300 bg-white">
<table class="w-full data-table text-xs">
<thead class="sticky top-0 bg-amber-100 z-10">
<tr>
<th class="w-40">일시</th>
<th class="w-24">단계</th>
<th>지정판매소</th>
<th class="w-14">주문</th>
<th>봉투 종류</th>
<th class="w-44">봉투 바코드</th>
<th class="w-14">포장</th>
<th class="w-14">수량</th>
</tr>
</thead>
<tbody id="dev-all-sales-tbody">
<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>
</tbody>
</table>
</div>
</section>
<script>
(function () {
// base_url() 은 .env 의 app.baseURL 기준 절대 URL을 만든다. 사용자가 다른 host(예: localhost)로
// 접속한 경우 cross-origin 으로 가 세션 쿠키가 빠진다. 페이지 origin과 동일하도록 path 부분만 사용.
const apiPath = '<?= parse_url(base_url('bag/sale/dev-all-sales-history'), PHP_URL_PATH) ?>';
const api = apiPath || '/bag/sale/dev-all-sales-history';
const tbody = document.getElementById('dev-all-sales-tbody');
const countEl = document.getElementById('dev-all-sales-count');
const refreshBtn = document.getElementById('dev-all-sales-refresh');
if (!tbody) return;
const nf = new Intl.NumberFormat('ko-KR');
const escapeHtml = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
}[c]));
function stageBadge(type) {
const t = String(type || '').toLowerCase();
if (t === 'order') {
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-sky-100 text-sky-800">주문 접수</span>';
}
if (t === 'sale') {
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-rose-100 text-rose-800">판매</span>';
}
if (t === 'returned') {
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800">반품 복귀</span>';
}
return `<span class="text-[12px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-700">${escapeHtml(type)}</span>`;
}
function rowClass(type) {
const t = String(type || '').toLowerCase();
if (t === 'returned') return 'text-gray-500';
return '';
}
function dash(s) {
return (s == null || String(s) === '') ? '<span class="text-gray-300">-</span>' : escapeHtml(s);
}
function renderRows(rows) {
if (!Array.isArray(rows) || rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">처리 내역이 없습니다.</td></tr>';
return;
}
const html = rows.map((r) => {
const shopText = (r.ds_name && String(r.ds_name).trim() !== '')
? `${escapeHtml(r.ds_shop_no || '')} ${escapeHtml(r.ds_name)}`.trim()
: `판매소#${escapeHtml(r.ds_idx || '0')}`;
const bagText = (r.bag_code || r.bag_name)
? `${escapeHtml(r.bag_code || '')} ${escapeHtml(r.bag_name || '')}`.trim()
: '<span class="text-gray-300">-</span>';
return `
<tr class="${rowClass(r.event_type)}">
<td class="text-center whitespace-nowrap">${escapeHtml(r.event_time || '')}</td>
<td class="text-center">${stageBadge(r.event_type)}</td>
<td class="text-left pl-1">${shopText}</td>
<td class="text-center">${escapeHtml(r.so_idx || '')}</td>
<td class="text-left pl-1">${bagText}</td>
<td class="text-center font-mono">${dash(r.code)}</td>
<td class="text-center">${dash(r.unit)}</td>
<td class="text-right pr-1 tabular-nums">${Number(r.qty || 0) ? nf.format(Number(r.qty)) : '<span class="text-gray-300">-</span>'}</td>
</tr>`;
}).join('');
tbody.innerHTML = html;
}
async function load() {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>';
countEl.textContent = '';
try {
const url = api + (api.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
const res = await fetch(url, {
credentials: 'same-origin',
cache: 'no-store',
headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' },
});
const data = await res.json();
if (!data || data.ok === false) {
const msg = (data && data.message) ? data.message : '내역을 불러오지 못했습니다.';
tbody.innerHTML = `<tr><td colspan="8" class="text-center text-red-500 py-4">${escapeHtml(msg)}</td></tr>`;
return;
}
// ok=false (예: 지자체 미선택)일 때는 진단을 표 자리에 띄운다.
if (data.ok === false) {
const sess = data.session || {};
const lines = [
(data.message || '지자체를 선택해 주세요.'),
`세션: mb_idx=${sess.mb_idx ?? '-'} · mb_level=${sess.mb_level ?? '-'} · admin_selected_lg_idx=${sess.admin_selected_lg_idx ?? '-'} · mb_lg_idx=${sess.mb_lg_idx ?? '-'}`,
];
tbody.innerHTML = `<tr><td colspan="8" class="text-left px-2 py-4 text-amber-900"><div class="mb-1">${escapeHtml(lines[0])}</div><div class="text-[12px] text-gray-500 font-mono">${escapeHtml(lines[1])}</div></td></tr>`;
countEl.textContent = `lg_idx=- · 주문 0 / 판매 0 / 반품복귀 0 · 표시 0건`;
return;
}
renderRows(data.rows || []);
const orders = Number(data.orders ?? 0);
const sold = Number(data.sold ?? 0);
const ret = Number(data.returned ?? 0);
const shown = Array.isArray(data.rows) ? data.rows.length : 0;
const lg = data.lg_idx == null ? '-' : data.lg_idx;
const sess = data.session || {};
const sessText = `[mb_level=${sess.mb_level ?? '-'}, admin=${sess.admin_selected_lg_idx ?? '-'}, mb_lg=${sess.mb_lg_idx ?? '-'}]`;
countEl.textContent = `lg_idx=${lg} ${sessText} · 주문 ${nf.format(orders)} / 판매 ${nf.format(sold)} / 반품복귀 ${nf.format(ret)} · 표시 ${nf.format(shown)}건`;
} catch (e) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-red-500 py-4">통신 오류로 불러오지 못했습니다.</td></tr>';
}
}
refreshBtn?.addEventListener('click', () => load());
// 폼 제출(주문 저장·판매 저장 등) 직후 redirect 되어 새로 로드된 경우에도
// 항상 최신 상태를 보장한다. pageshow(BFCache 복원 포함)에서 한 번 더 호출.
window.addEventListener('pageshow', (ev) => {
if (ev.persisted) load();
});
load();
})();
</script>