feat: '발주파일 생성'(구 LOT-No 디스켓 불출) 이름변경 + 생성 전 내용 확인

- 메뉴/화면/매뉴얼 명칭을 '발주파일 생성'으로 변경(메뉴 rename 마이그레이션 SQL 포함)
- 발주파일(seed) 생성 전 '내용 확인' 미리보기 추가(암호화 전 발주 메타·품목 표시)
  · GET bag/order/lot-seed/preview 신설, 생성/미리보기 데이터 구성 공용화

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-07-06 18:26:58 +09:00
parent a5d995588b
commit de15cc9e47
9 changed files with 270 additions and 33 deletions

View File

@@ -21,7 +21,7 @@ for ($year = $baseYear - 2; $year <= $baseYear + 2; $year++) {
?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">LOT-No 디스켓 불출</span>
<span class="text-sm font-bold text-gray-700">발주파일 생성</span>
</section>
<section class="border border-gray-300 rounded-lg p-2 mt-2 bg-white">
@@ -92,11 +92,12 @@ for ($year = $baseYear - 2; $year <= $baseYear + 2; $year++) {
?>
<tr>
<td class="text-center">
<form method="post" action="<?= base_url('bag/order/lot-seed/generate') ?>" class="inline">
<?= csrf_field() ?>
<input type="hidden" name="bo_idx" value="<?= $boIdx ?>" />
<button type="submit" class="text-blue-600 hover:underline text-xs">seed 생성</button>
</form>
<button type="button"
class="lot-preview-btn text-blue-600 hover:underline text-xs"
data-bo-idx="<?= $boIdx ?>"
data-lot="<?= esc((string) ($order->bo_lot_no ?? ''), 'attr') ?>">
내용 확인
</button>
</td>
<td class="text-center"><?= esc((string) ($order->bo_order_date ?? '')) ?></td>
<td class="text-center font-mono"><?= esc((string) ($order->bo_lot_no ?? '')) ?></td>
@@ -128,3 +129,133 @@ for ($year = $baseYear - 2; $year <= $baseYear + 2; $year++) {
<?php if (isset($pager)): ?>
<div class="mt-2 mb-2 no-print"><?= $pager->links() ?></div>
<?php endif; ?>
<!-- 발주파일 생성 전 '내용 확인' 모달 -->
<div id="lot-preview-overlay" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/40 p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-3xl max-h-[85vh] flex flex-col">
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-2">
<span class="text-sm font-bold text-gray-800">발주파일 내용 확인</span>
<button type="button" id="lot-preview-close" class="text-gray-400 hover:text-gray-700 text-lg leading-none">&times;</button>
</div>
<div class="p-4 overflow-auto text-sm">
<div id="lot-preview-loading" class="text-center text-gray-400 py-8">불러오는 중…</div>
<div id="lot-preview-error" class="hidden text-center text-red-600 py-8"></div>
<div id="lot-preview-body" class="hidden">
<p class="text-xs text-gray-500 mb-2">아래 내용으로 <b>암호화된 발주파일(.seed.json)</b>이 생성·다운로드됩니다. 생성 전 내용을 확인하세요.</p>
<dl class="grid grid-cols-[6rem_1fr] gap-y-1 text-[13px] mb-3 border border-gray-200 rounded p-2 bg-gray-50">
<dt class="font-semibold text-gray-600">LOT-No</dt><dd class="font-mono" id="lp-lot"></dd>
<dt class="font-semibold text-gray-600">발주일</dt><dd id="lp-date"></dd>
<dt class="font-semibold text-gray-600">제작업체</dt><dd id="lp-company"></dd>
<dt class="font-semibold text-gray-600">버전</dt><dd id="lp-version"></dd>
<dt class="font-semibold text-gray-600">무결성해시</dt><dd class="font-mono text-[11px] text-gray-500 break-all" id="lp-hash"></dd>
</dl>
<div class="overflow-auto border border-gray-200 rounded">
<table class="w-full data-table text-[13px]">
<thead>
<tr>
<th class="text-left pl-2">봉투코드</th>
<th class="text-left">봉투명</th>
<th class="w-20 text-right">단가</th>
<th class="w-16 text-right">박스</th>
<th class="w-16 text-right">팩</th>
<th class="w-20 text-right">낱장</th>
<th class="w-24 text-right pr-2">금액</th>
</tr>
</thead>
<tbody id="lp-items"></tbody>
<tfoot>
<tr class="font-semibold bg-gray-50">
<td class="pl-2" colspan="3">합계 (<span id="lp-line-count">0</span>품목)</td>
<td class="text-right" id="lp-sum-box">0</td>
<td class="text-right">—</td>
<td class="text-right" id="lp-sum-sheet">0</td>
<td class="text-right pr-2" id="lp-sum-amount">0</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="border-t border-gray-200 px-4 py-2 flex items-center justify-end gap-2">
<button type="button" id="lot-preview-cancel" class="border border-gray-300 text-gray-600 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">닫기</button>
<form method="post" action="<?= base_url('bag/order/lot-seed/generate') ?>" class="inline" id="lot-generate-form">
<?= csrf_field() ?>
<input type="hidden" name="bo_idx" id="lot-generate-bo-idx" value="" />
<button type="submit" id="lot-generate-submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 disabled:opacity-50" disabled>발주파일 생성(다운로드)</button>
</form>
</div>
</div>
</div>
<script>
(function () {
var previewUrl = '<?= base_url('bag/order/lot-seed/preview') ?>';
var overlay = document.getElementById('lot-preview-overlay');
var elLoading = document.getElementById('lot-preview-loading');
var elError = document.getElementById('lot-preview-error');
var elBody = document.getElementById('lot-preview-body');
var elSubmit = document.getElementById('lot-generate-submit');
var elBoIdx = document.getElementById('lot-generate-bo-idx');
var nf = function (n) { return new Intl.NumberFormat('ko-KR').format(n || 0); };
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' })[c]; }); };
function openModal() { overlay.classList.remove('hidden'); overlay.classList.add('flex'); }
function closeModal() { overlay.classList.add('hidden'); overlay.classList.remove('flex'); }
function showState(state) {
elLoading.classList.toggle('hidden', state !== 'loading');
elError.classList.toggle('hidden', state !== 'error');
elBody.classList.toggle('hidden', state !== 'body');
elSubmit.disabled = state !== 'body';
}
document.querySelectorAll('.lot-preview-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var boIdx = btn.getAttribute('data-bo-idx');
elBoIdx.value = boIdx;
openModal();
showState('loading');
fetch(previewUrl + '?bo_idx=' + encodeURIComponent(boIdx), { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok) {
elError.textContent = (d && d.error) || '내용을 불러오지 못했습니다.';
showState('error');
return;
}
document.getElementById('lp-lot').textContent = d.lot_no || '';
document.getElementById('lp-date').textContent = d.order_date || '';
document.getElementById('lp-company').textContent = d.company || '-';
document.getElementById('lp-version').textContent = 'v' + (d.version || 1);
document.getElementById('lp-hash').textContent = d.order_hash || '';
var rows = (d.items || []).map(function (it) {
return '<tr>'
+ '<td class="pl-2 font-mono">' + esc(it.bag_code) + '</td>'
+ '<td>' + esc(it.bag_name) + '</td>'
+ '<td class="text-right">' + nf(it.unit_price) + '</td>'
+ '<td class="text-right">' + nf(it.qty_box) + '</td>'
+ '<td class="text-right">' + nf(it.qty_pack) + '</td>'
+ '<td class="text-right">' + nf(it.qty_sheet) + '</td>'
+ '<td class="text-right pr-2">' + nf(it.amount) + '</td>'
+ '</tr>';
}).join('');
document.getElementById('lp-items').innerHTML = rows || '<tr><td colspan="7" class="text-center text-gray-400 py-4">품목이 없습니다.</td></tr>';
var t = d.totals || {};
document.getElementById('lp-line-count').textContent = nf(t.line_count);
document.getElementById('lp-sum-box').textContent = nf(t.qty_box);
document.getElementById('lp-sum-sheet').textContent = nf(t.qty_sheet);
document.getElementById('lp-sum-amount').textContent = nf(t.amount);
showState('body');
})
.catch(function () {
elError.textContent = '통신 오류가 발생했습니다.';
showState('error');
});
});
});
document.getElementById('lot-preview-close').addEventListener('click', closeModal);
document.getElementById('lot-preview-cancel').addEventListener('click', closeModal);
overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); });
})();
</script>