feat: 발주 등록/변경/삭제 화면 개선 + 발주담당 추가

- 발주 등록: 폼(좌)·발주품목 선택(우, 스크롤)·발주 이력(하단) 재배치, 수량→박스수량
- 발주담당(담당자) 선택 항목 추가(bo_manager_idx), 등록/변경 시 저장
- 발주 이력 발주일 클릭 → 발주 내역 보기 모달(변경/삭제 버튼), 삭제·취소 발주는 두 버튼 숨김
- 발주 변경: 변경구분 라디오 제거, 원 발주번호(LOT)·이력 안내 표시, 취소 시 발주 등록 화면으로 이동
- 발주 등록 화면에서도 삭제 가능(정상 발주만), 삭제 후 등록 화면 복귀
- 삭제 화면에서 삭제 대상 발주 내역 표시
- 발주 상세 JSON 엔드포인트(bag/order/detail-json) 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-07-06 18:39:32 +09:00
parent 952ca3e3e7
commit 26384dbe2a
6 changed files with 297 additions and 64 deletions

View File

@@ -114,7 +114,7 @@
<tr class="<?= $rowNormal ? '' : 'opacity-60' ?>">
<td class="text-center align-middle">
<?php if ($rowNormal): ?>
<input type="radio" name="bo_idx" value="<?= (int) $history->bo_idx ?>" class="accent-red-600" <?= (int) $history->bo_idx === $selectedDeleteBoIdx ? 'checked' : '' ?> <?= $deleteRadioFirst ? 'required' : '' ?> />
<input type="radio" name="bo_idx" value="<?= (int) $history->bo_idx ?>" class="accent-red-600 js-delete-pick" <?= (int) $history->bo_idx === $selectedDeleteBoIdx ? 'checked' : '' ?> <?= $deleteRadioFirst ? 'required' : '' ?> />
<?php $deleteRadioFirst = false; ?>
<?php else: ?>
<span class="text-gray-400" title="삭제할 수 없는 상태">—</span>
@@ -135,7 +135,8 @@
</section>
<section class="xl:col-span-7 border border-red-200 bg-red-50 p-4">
<p class="text-sm font-bold text-red-800 mb-2">발주 삭제</p>
<p class="text-sm text-gray-700 mb-4">목록에서 선택한 발주를 삭제 처리합니다. 계속하시겠습니까?</p>
<p class="text-sm text-gray-700 mb-3">아래 <b>삭제 대상 발주 내역</b>을 확인한 뒤 「삭제 실행」을 누르세요.</p>
<div id="delete-detail" class="bg-white border border-red-200 rounded p-3 text-sm mb-4 max-h-[320px] overflow-auto">삭제할 발주를 선택하세요.</div>
<div class="flex flex-wrap gap-2 items-center">
<button type="submit" class="bg-red-600 text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90" <?= $firstNormalBoIdx === null ? 'disabled' : '' ?>>삭제 실행</button>
<a href="<?= base_url('bag/order/change?month=' . rawurlencode($orderReturnMonth !== '' ? $orderReturnMonth : substr($defaultOrderDate, 0, 7))) ?>" class="bg-gray-200 text-gray-800 px-6 py-1.5 rounded-sm text-sm">취소</a>
@@ -143,28 +144,64 @@
</section>
</div>
</form>
<script>
(() => {
const detailBox = document.getElementById('delete-detail');
if (!detailBox) return;
const detailBase = new URL(<?= json_encode(base_url('bag/order/detail-json'), JSON_UNESCAPED_SLASHES) ?>, window.location.href).pathname;
const nf = (n) => new Intl.NumberFormat('ko-KR').format(Number(n) || 0);
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
const load = (id) => {
if (!id) { detailBox.innerHTML = '삭제할 발주를 선택하세요.'; return; }
detailBox.innerHTML = '불러오는 중…';
fetch(detailBase + '/' + id, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then((r) => r.json())
.then((d) => {
if (!d || !d.ok) { detailBox.innerHTML = '<span class="text-red-600">' + esc((d && d.error) || '불러오지 못했습니다.') + '</span>'; return; }
const rows = (d.items || []).map((it, i) => `<tr>
<td class="text-center">${i + 1}</td>
<td class="text-left pl-2">${esc(it.bag_name || it.bag_code)}</td>
<td class="text-right pr-2">${nf(it.qty_box)}</td>
<td class="text-right pr-2">${nf(it.qty_sheet)}</td>
<td class="text-right pr-2">${nf(it.amount)}</td></tr>`).join('');
detailBox.innerHTML = `
<dl class="grid grid-cols-[6rem_1fr] gap-y-0.5 mb-2">
<dt class="font-semibold text-gray-600">발주번호</dt><dd class="font-mono">${esc(d.bo_lot_no)} <span class="text-gray-400">(v${d.bo_version})</span></dd>
<dt class="font-semibold text-gray-600">발주일</dt><dd>${esc(d.order_date)}</dd>
<dt class="font-semibold text-gray-600">제작업체</dt><dd>${esc(d.company || '-')}</dd>
<dt class="font-semibold text-gray-600">입고처</dt><dd>${esc(d.agency || '-')}</dd>
</dl>
<table class="w-full data-table text-sm">
<thead><tr><th class="w-8 text-center">번호</th><th class="text-left">품명</th><th class="text-right">박스수량</th><th class="text-right">낱장</th><th class="text-right">금액</th></tr></thead>
<tbody>${rows || '<tr><td colspan="5" class="text-center text-gray-400 py-3">품목이 없습니다.</td></tr>'}</tbody>
</table>`;
})
.catch(() => { detailBox.innerHTML = '<span class="text-red-600">오류가 발생했습니다.</span>'; });
};
document.querySelectorAll('.js-delete-pick').forEach((r) => {
r.addEventListener('change', () => { if (r.checked) load(r.value); });
});
const checked = document.querySelector('.js-delete-pick:checked');
if (checked) load(checked.value);
})();
</script>
<?php else: ?>
<form action="<?= base_url('bag/order/store') ?>" method="POST" class="mt-2 space-y-2" id="bag-order-store-form">
<?= csrf_field() ?>
<?php if ($editMode): ?>
<input type="hidden" name="bo_source_idx" value="<?= esc((string) ($editDefaults['bo_source_idx'] ?? 0)) ?>" />
<fieldset class="border border-gray-200 rounded px-3 py-2 mb-1 text-sm bg-white">
<legend class="text-xs font-bold text-gray-600 px-1">변경 구분</legend>
<div class="flex flex-wrap gap-x-4 gap-y-1">
<label class="inline-flex items-center gap-1 cursor-pointer">
<input type="radio" name="bo_change_mode" value="price" <?= $changeMode === 'price' ? 'checked' : '' ?> />
<span>발주·도매·판매 단가</span>
</label>
<label class="inline-flex items-center gap-1 cursor-pointer">
<input type="radio" name="bo_change_mode" value="meta" <?= $changeMode === 'meta' ? 'checked' : '' ?> />
<span>업체·수수료·협회·발주</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-1">
발주 삭제는 <a class="text-blue-600 hover:underline" href="<?= base_url('bag/order/revise/' . (int) ($editDefaults['bo_source_idx'] ?? 0) . '?change_mode=delete') ?>">발주 삭제 화면</a>으로 이동합니다.
</p>
</fieldset>
<input type="hidden" name="bo_change_mode" value="meta" />
<div class="border border-amber-300 rounded px-3 py-2 mb-1 text-sm bg-amber-50 flex flex-wrap items-center gap-x-3 gap-y-1">
<span class="font-bold text-gray-700">원 발주번호(LOT)</span>
<span class="font-mono text-gray-900"><?= esc($orderLotNo !== '' ? $orderLotNo : '-') ?></span>
<span class="text-gray-500">발주 #<?= (int) ($editDefaults['bo_source_idx'] ?? 0) ?></span>
<span class="text-gray-500">— 변경 저장 시 원 발주번호는 유지되고, 이전 내용은 이력(버전)으로 보존됩니다.</span>
<a class="text-red-600 hover:underline ml-auto" href="<?= base_url('bag/order/revise/' . (int) ($editDefaults['bo_source_idx'] ?? 0) . '?change_mode=delete') ?>">발주 삭제</a>
</div>
<?php endif; ?>
<?php if ($hubReturn && $orderReturnMonth !== ''): ?>
<input type="hidden" name="order_return_hub" value="1" />
@@ -188,48 +225,26 @@
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2">
<section class="xl:col-span-5 border border-gray-300 rounded-lg overflow-hidden bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 이력</div>
<div class="overflow-auto max-h-[410px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-28 text-center">발주일</th>
<th class="text-left">제작업체</th>
<th class="text-left">입고처</th>
<th class="w-16 text-center">상태</th>
</tr>
</thead>
<tbody>
<?php foreach (($recentOrders ?? []) as $history): ?>
<tr>
<td class="text-center">
<a href="<?= base_url('bag/order/revise/' . (int) $history->bo_idx) ?>" class="text-blue-600 hover:underline">
<?= esc((string) $history->bo_order_date) ?>
</a>
</td>
<td class="text-left pl-2"><?= esc((string) ($companyMap[(int) $history->bo_company_idx] ?? '-')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($agencyMap[(int) $history->bo_agency_idx] ?? '-')) ?></td>
<td class="text-center"><?= esc((string) ($statusMap[(string) $history->bo_status] ?? $history->bo_status)) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">발주 이력이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2 items-start">
<section class="xl:col-span-7 border border-gray-300 rounded-lg overflow-hidden bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700"><?= $editMode ? '발주 변경 수정' : '발주 Form' ?></div>
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700"><?= $editMode ? '발주 변경 수정' : '발주등록' ?></div>
<div class="p-2 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div class="flex items-center gap-2 text-sm">
<label for="bo_order_date" class="w-20 font-bold text-gray-700">발주일 <span class="text-red-500">*</span></label>
<input id="bo_order_date" name="bo_order_date" type="date" value="<?= esc($defaultOrderDate) ?>" required class="border border-gray-300 rounded px-2 py-1 w-full" />
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_manager_idx" class="w-20 font-bold text-gray-700">발주담당</label>
<select id="bo_manager_idx" name="bo_manager_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($managers ?? []) as $manager): ?>
<option value="<?= esc((string) $manager->mg_idx) ?>" <?= (int) old('bo_manager_idx', $editDefaults['bo_manager_idx'] ?? 0) === (int) $manager->mg_idx ? 'selected' : '' ?>>
<?= esc((string) $manager->mg_name) ?><?= ($manager->mg_affiliation ?? '') !== '' ? ' (' . esc((string) $manager->mg_affiliation) . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_association_idx" class="w-20 font-bold text-gray-700">협회</label>
<select id="bo_association_idx" name="bo_association_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
@@ -272,7 +287,7 @@
<th class="w-12 text-center">번호</th>
<th class="w-16 text-center">선택</th>
<th class="text-left">품명</th>
<th class="w-24 text-right">수량</th>
<th class="w-24 text-right">박스수량</th>
<th class="w-24 text-right">단가</th>
<th class="w-24 text-right">낱장환산</th>
<?php if ($editMode): ?>
@@ -324,16 +339,17 @@
</div>
<div class="flex gap-2 pt-1">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition"><?= $editMode ? '변경 저장' : '발주' ?></button>
<a href="<?= base_url('bag/purchase-inbound') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition"><?= $editMode ? '변경 저장' : '발주' ?></button>
<?php if ($editMode): ?>
<a href="<?= base_url('bag/order/create') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<?php endif; ?>
</div>
</div>
</section>
</div>
<section class="border border-gray-300 rounded-lg overflow-hidden bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 등록 종류</div>
<div class="overflow-auto">
<section class="xl:col-span-5 border border-gray-300 rounded-lg overflow-hidden bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주품목 선택</div>
<div class="overflow-auto max-h-[560px]">
<table class="w-full data-table text-sm order-reference-table">
<thead>
<tr>
@@ -365,6 +381,40 @@
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</div>
<section class="border border-gray-300 rounded-lg overflow-hidden bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 이력</div>
<div class="overflow-auto max-h-[410px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-28 text-center">발주일</th>
<th class="text-left">제작업체</th>
<th class="text-left">입고처</th>
<th class="w-16 text-center">상태</th>
</tr>
</thead>
<tbody>
<?php foreach (($recentOrders ?? []) as $history): ?>
<tr>
<td class="text-center">
<button type="button" class="js-order-view text-blue-600 hover:underline font-medium" data-order-id="<?= (int) $history->bo_idx ?>" title="발주 내역 보기">
<?= esc((string) $history->bo_order_date) ?>
</button>
</td>
<td class="text-left pl-2"><?= esc((string) ($companyMap[(int) $history->bo_company_idx] ?? '-')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($agencyMap[(int) $history->bo_agency_idx] ?? '-')) ?></td>
<td class="text-center"><?= esc((string) ($statusMap[(string) $history->bo_status] ?? $history->bo_status)) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">발주 이력이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</form>
@@ -384,6 +434,96 @@
}
</style>
<!-- 발주 내역 보기 모달 -->
<div id="order-view-modal" class="hidden fixed inset-0 z-50 items-center justify-center" style="background:rgba(0,0,0,.4)">
<div class="bg-white rounded-lg shadow-xl w-[680px] max-w-[95vw] max-h-[85vh] overflow-auto">
<div class="flex items-center justify-between border-b px-4 py-2">
<span class="font-bold text-gray-800">발주 내역</span>
<button type="button" id="order-view-close" class="text-gray-500 hover:text-gray-800 text-lg leading-none">✕</button>
</div>
<div class="p-4 text-sm" id="order-view-body">불러오는 중…</div>
<div class="flex items-center justify-end gap-2 border-t px-4 py-2">
<form id="order-view-delete-form" action="<?= base_url('bag/order/delete') ?>" method="post" class="mr-auto">
<?= csrf_field() ?>
<input type="hidden" name="bo_idx" id="order-view-delete-id" value="" />
<input type="hidden" name="return_to" value="create" />
<button type="submit" id="order-view-delete" class="hidden bg-red-600 text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">삭제</button>
</form>
<a id="order-view-edit" href="#" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">발주변경</a>
<button type="button" id="order-view-close2" class="bg-gray-200 text-gray-700 px-4 py-1.5 rounded-sm text-sm hover:bg-gray-300">닫기</button>
</div>
</div>
</div>
<script>
(() => {
const modal = document.getElementById('order-view-modal');
if (!modal) return;
const body = document.getElementById('order-view-body');
const editLink = document.getElementById('order-view-edit');
const deleteBtn = document.getElementById('order-view-delete');
const deleteIdInput = document.getElementById('order-view-delete-id');
const deleteForm = document.getElementById('order-view-delete-form');
const reviseBase = <?= json_encode(base_url('bag/order/revise'), JSON_UNESCAPED_SLASHES) ?>;
const detailBase = new URL(<?= json_encode(base_url('bag/order/detail-json'), JSON_UNESCAPED_SLASHES) ?>, window.location.href).pathname;
const nf = (n) => new Intl.NumberFormat('ko-KR').format(Number(n) || 0);
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
const open = () => { modal.classList.remove('hidden'); modal.style.display = 'flex'; };
const close = () => { modal.classList.add('hidden'); modal.style.display = 'none'; };
document.getElementById('order-view-close')?.addEventListener('click', close);
document.getElementById('order-view-close2')?.addEventListener('click', close);
modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
if (deleteForm) {
deleteForm.addEventListener('submit', (e) => {
if (!confirm('이 발주를 삭제하시겠습니까?')) { e.preventDefault(); }
});
}
document.querySelectorAll('.js-order-view').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.dataset.orderId;
if (!id) return;
editLink.href = reviseBase + '/' + id;
if (deleteIdInput) deleteIdInput.value = id;
if (deleteBtn) deleteBtn.classList.add('hidden'); // 상태 확인 전까지 숨김
if (editLink) editLink.classList.add('hidden'); // 상태 확인 전까지 숨김
body.innerHTML = '불러오는 중…';
open();
fetch(detailBase + '/' + id, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then((r) => r.json())
.then((d) => {
if (!d || !d.ok) { body.innerHTML = '<p class="text-red-600">' + esc((d && d.error) || '불러오지 못했습니다.') + '</p>'; return; }
// 정상 상태 발주만 삭제·변경 가능 (삭제/취소된 발주는 두 버튼 모두 숨김)
const isNormal = d.status === 'normal';
if (deleteBtn) deleteBtn.classList.toggle('hidden', !isNormal);
if (editLink) editLink.classList.toggle('hidden', !isNormal);
const rows = (d.items || []).map((it, i) => `<tr>
<td class="text-center">${i + 1}</td>
<td class="text-left pl-2">${esc(it.bag_name || it.bag_code)}</td>
<td class="text-right pr-2">${nf(it.qty_box)}</td>
<td class="text-right pr-2">${nf(it.qty_sheet)}</td>
<td class="text-right pr-2">${nf(it.unit_price)}</td>
<td class="text-right pr-2">${nf(it.amount)}</td></tr>`).join('');
body.innerHTML = `
<dl class="grid grid-cols-[7rem_1fr] gap-y-1 mb-3">
<dt class="font-semibold text-gray-600">발주번호(LOT)</dt><dd class="font-mono">${esc(d.bo_lot_no)} <span class="text-gray-400">(v${d.bo_version})</span></dd>
<dt class="font-semibold text-gray-600">발주일</dt><dd>${esc(d.order_date)}</dd>
<dt class="font-semibold text-gray-600">제작업체</dt><dd>${esc(d.company || '-')}</dd>
<dt class="font-semibold text-gray-600">입고처</dt><dd>${esc(d.agency || '-')}</dd>
<dt class="font-semibold text-gray-600">발주담당</dt><dd>${esc(d.manager || '-')}</dd>
<dt class="font-semibold text-gray-600">조달수수료</dt><dd>${nf(d.fee_rate)}%</dd>
</dl>
<table class="w-full data-table text-sm">
<thead><tr><th class="w-10 text-center">번호</th><th class="text-left">품명</th><th class="text-right">박스수량</th><th class="text-right">낱장</th><th class="text-right">단가</th><th class="text-right">금액</th></tr></thead>
<tbody>${rows || '<tr><td colspan="6" class="text-center text-gray-400 py-3">품목이 없습니다.</td></tr>'}</tbody>
</table>`;
})
.catch(() => { body.innerHTML = '<p class="text-red-600">오류가 발생했습니다.</p>'; });
});
});
})();
</script>
<script>
(() => {
const bagMeta = <?= json_encode($bagMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
@@ -548,7 +688,7 @@
const renderSelectedRows = () => {
const codes = Object.keys(bagMeta).filter((code) => selectedItems.has(code));
if (codes.length === 0) {
selectedBody.innerHTML = `<tr><td colspan="${colEmpty}" class="text-center text-gray-400 py-4">아래 "발주 등록 종류"에서 봉투를 선택해 주세요.</td></tr>`;
selectedBody.innerHTML = `<tr><td colspan="${colEmpty}" class="text-center text-gray-400 py-4">우측 "발주품목 선택"에서 봉투를 선택해 주세요.</td></tr>`;
setActiveRow(null);
updateTotals();
updateReferenceSelectionUi();