사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.

통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
taekyoungc
2026-06-01 16:15:15 +09:00
parent 21e7b91871
commit 0f1d414f37
129 changed files with 18068 additions and 1585 deletions

View File

@@ -1,54 +1,409 @@
<div class="space-y-1">
<div class="flex items-center justify-between mb-1">
<form method="get" class="flex items-center gap-3 text-sm">
<label class="font-bold text-gray-700">불출일</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<span>~</span>
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
<a href="<?= base_url('bag/issue') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
<?= view('components/print_header', ['printTitle' => '무료용 불출 취소']) ?>
<?php
$filters = is_array($filters ?? null) ? $filters : [];
$issueMonth = (string) ($filters['issue_month'] ?? '');
$destName = (string) ($filters['dest_name'] ?? '');
$issueType = (string) ($filters['issue_type'] ?? '');
$bagCode = (string) ($filters['bag_code'] ?? '');
$selectedGroupDate = (string) ($selectedGroupDate ?? '');
$selectedGroupDest = (string) ($selectedGroupDest ?? '');
$selectedIssueId = (int) ($selectedIssueId ?? 0);
$selectedBagCode = (string) ($selectedBagCode ?? '');
?>
<div class="space-y-2">
<div class="border border-gray-300 bg-white p-2">
<form method="get" class="flex flex-wrap items-end gap-2 text-sm">
<label class="font-bold text-gray-700">불출월</label>
<select name="issue_month" class="border border-gray-300 rounded px-2 py-1 min-w-[9rem]">
<option value="">전체</option>
<?php foreach (($monthOptions ?? []) as $month): ?>
<option value="<?= esc($month) ?>" <?= $issueMonth === $month ? 'selected' : '' ?>><?= esc($month) ?></option>
<?php endforeach; ?>
</select>
<label class="font-bold text-gray-700">불출처</label>
<select name="dest_name" class="border border-gray-300 rounded px-2 py-1 min-w-[10rem]">
<option value="">전체</option>
<?php foreach (($destOptions ?? []) as $opt): ?>
<option value="<?= esc($opt) ?>" <?= $destName === $opt ? 'selected' : '' ?>><?= esc($opt) ?></option>
<?php endforeach; ?>
</select>
<label class="font-bold text-gray-700">불출구분</label>
<select name="issue_type" class="border border-gray-300 rounded px-2 py-1 min-w-[8rem]">
<option value="">전체</option>
<?php foreach (($typeOptions ?? []) as $opt): ?>
<option value="<?= esc($opt) ?>" <?= $issueType === $opt ? 'selected' : '' ?>><?= esc($opt) ?></option>
<?php endforeach; ?>
</select>
<label class="font-bold text-gray-700">봉투종류</label>
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 min-w-[12rem]">
<option value="">전체</option>
<?php foreach (($bagOptions ?? []) as $opt): ?>
<?php $code = (string) ($opt['code'] ?? ''); ?>
<option value="<?= esc($code) ?>" <?= $bagCode === $code ? 'selected' : '' ?>>
<?= esc($code) ?> - <?= esc((string) ($opt['name'] ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
<input type="hidden" name="sel_date" value="<?= esc($selectedGroupDate) ?>"/>
<input type="hidden" name="sel_dest" value="<?= esc($selectedGroupDest) ?>"/>
<input type="hidden" name="sel_issue_id" value="<?= esc((string) $selectedIssueId) ?>"/>
<input type="hidden" name="sel_bag_code" value="<?= esc($selectedBagCode) ?>"/>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
<a href="<?= base_url('bag/issue/cancel') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
<button type="button" onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-4 py-1 rounded-sm hover:bg-gray-50">인쇄</button>
<a href="<?= base_url('bag/issue/create') ?>" class="bg-btn-search text-white px-3 py-1 rounded-sm">불출 처리</a>
</form>
<a href="<?= base_url('bag/issue/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
</div>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>연도</th><th>분기</th><th>구분</th><th>불출일</th><th>불출처</th><th>봉투코드</th><th>봉투명</th><th>수량</th><th>상태</th><th>작업</th>
</tr></thead>
<tbody>
<?php if (! empty($list)): ?>
<?php foreach ($list as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->bi2_year ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_quarter ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_issue_type ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_issue_date ?? '') ?></td>
<td><?= esc($row->bi2_dest_name ?? '') ?></td>
<td class="text-center"><?= esc($row->bi2_bag_code ?? '') ?></td>
<td><?= esc($row->bi2_bag_name ?? '') ?></td>
<td class="text-right"><?= number_format((int)($row->bi2_qty ?? 0)) ?></td>
<td class="text-center">
<?php
$st = $row->bi2_status ?? 'normal';
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', default => esc($st) };
?>
</td>
<td class="text-center">
<?php if (($row->bi2_status ?? '') === 'normal'): ?>
<form method="post" action="<?= base_url('bag/issue/cancel/' . $row->bi2_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
<?= csrf_field() ?>
<button class="text-orange-600 hover:underline text-xs">취소</button>
</form>
<?php else: ?>
-
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="11" class="text-center text-gray-400 py-4">등록된 불출이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
<form method="post" action="<?= base_url('bag/issue/cancel-save') ?>" id="issue-cancel-form">
<?= csrf_field() ?>
<input type="hidden" name="issue_month" value="<?= esc($issueMonth) ?>"/>
<input type="hidden" name="dest_name" value="<?= esc($destName) ?>"/>
<input type="hidden" name="issue_type" value="<?= esc($issueType) ?>"/>
<input type="hidden" name="bag_code" value="<?= esc($bagCode) ?>"/>
<input type="hidden" name="sel_date" value="<?= esc($selectedGroupDate) ?>"/>
<input type="hidden" name="sel_dest" value="<?= esc($selectedGroupDest) ?>"/>
<input type="hidden" name="sel_issue_id" value="<?= esc((string) $selectedIssueId) ?>"/>
<input type="hidden" name="sel_bag_code" value="<?= esc($selectedBagCode) ?>"/>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-2">
<section class="border border-gray-300 bg-white xl:col-span-1">
<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">
<thead>
<tr>
<th class="w-20">불출일자</th>
<th>불출처</th>
<th class="w-16">건수</th>
</tr>
</thead>
<tbody>
<?php if (($issueGroups ?? []) !== []): ?>
<?php foreach (($issueGroups ?? []) as $row): ?>
<?php
$date = (string) ($row['bi2_issue_date'] ?? '');
$dest = (string) ($row['bi2_dest_name'] ?? '');
$isSelected = ($date === $selectedGroupDate && $dest === $selectedGroupDest);
$url = base_url('bag/issue/cancel?' . http_build_query([
'issue_month' => $issueMonth,
'dest_name' => $destName,
'issue_type' => $issueType,
'bag_code' => $bagCode,
'sel_date' => $date,
'sel_dest' => $dest,
]));
?>
<tr
class="<?= $isSelected ? 'bg-blue-100 font-semibold' : '' ?> cursor-pointer hover:bg-blue-50"
onclick="window.location.href='<?= esc($url, 'attr') ?>'"
>
<td class="text-center <?= $isSelected ? 'border-l-4 border-blue-600' : '' ?>">
<?= esc($date) ?>
</td>
<td class="pl-2"><?= esc($dest) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['row_count'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="3" class="text-center text-gray-400 py-4">조회 결과가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<div class="xl:col-span-3 grid grid-cols-1 gap-2">
<section class="border border-gray-300 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-[270px]">
<table class="w-full data-table text-sm" id="detail-table">
<thead>
<tr>
<th class="w-14">취소</th>
<th class="w-12">No</th>
<th class="w-24">일자</th>
<th class="w-20">구분</th>
<th>봉투종류</th>
<th class="w-24">수량</th>
<th class="w-24">취소수량</th>
</tr>
</thead>
<tbody>
<?php
$detailTotal = 0;
$detailCancelTotal = 0;
?>
<?php if (($detailRows ?? []) !== []): ?>
<?php foreach (($detailRows ?? []) as $idx => $row): ?>
<?php
$qty = (int) ($row['base_qty'] ?? 0);
$cancelQty = (int) ($row['cancel_qty'] ?? 0);
$detailTotal += $qty;
$detailCancelTotal += $cancelQty;
$isChecked = $cancelQty >= $qty && $qty > 0;
$rowBagCode = (string) ($row['bi2_bag_code'] ?? '');
$selectedLink = base_url('bag/issue/cancel?' . http_build_query([
'issue_month' => $issueMonth,
'dest_name' => $destName,
'issue_type' => $issueType,
'bag_code' => $bagCode,
'sel_date' => $selectedGroupDate,
'sel_dest' => $selectedGroupDest,
'sel_issue_id' => $selectedIssueId,
'sel_bag_code' => $rowBagCode,
]));
?>
<tr data-bag-code="<?= esc($rowBagCode, 'attr') ?>" class="<?= $selectedBagCode === $rowBagCode ? 'bg-blue-50' : '' ?>">
<td class="text-center">
<input type="checkbox" class="detail-check" value="1" <?= $isChecked ? 'checked' : '' ?>/>
</td>
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center"><?= esc((string) ($row['bi2_issue_date'] ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row['bi2_issue_type'] ?? '')) ?></td>
<td class="pl-2">
<a href="<?= esc($selectedLink) ?>" class="text-blue-700 hover:underline">
<?= esc((string) ($row['bi2_bag_name'] ?? '')) ?> (<?= esc((string) ($row['bi2_bag_code'] ?? '')) ?>)
</a>
</td>
<td class="text-right pr-2 detail-qty"><?= number_format($qty) ?></td>
<td class="text-right pr-2">
<input type="number" min="0" max="<?= esc((string) $qty) ?>" class="detail-cancel-input border border-gray-300 rounded px-1 py-0.5 w-24 text-right"
data-max="<?= esc((string) $qty) ?>"
data-bag-code="<?= esc($rowBagCode, 'attr') ?>"
value="<?= esc((string) $cancelQty) ?>"/>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">불출 품목 내역이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-50 font-semibold">
<td colspan="5" class="text-center">합계</td>
<td class="text-right pr-2" id="detail-total-qty"><?= number_format($detailTotal) ?></td>
<td class="text-right pr-2" id="detail-total-cancel"><?= number_format($detailCancelTotal) ?></td>
</tr>
</tfoot>
</table>
</div>
</section>
<section class="border border-gray-300 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-[250px]">
<table class="w-full data-table text-sm" id="code-table">
<thead>
<tr>
<th class="w-14">취소</th>
<th class="w-12">No</th>
<th class="w-20">불출번호</th>
<th>봉투코드</th>
<th class="w-24">수량</th>
<th class="w-24">취소수량</th>
</tr>
</thead>
<tbody>
<?php
$codeTotal = 0;
$codeCancelTotal = 0;
?>
<?php if (($codeRows ?? []) !== []): ?>
<?php foreach (($codeRows ?? []) as $idx => $row): ?>
<?php
$bicIdx = (int) ($row['bic_idx'] ?? 0);
$qty = (int) ($row['bic_qty'] ?? 0);
$cancelQty = (int) ($row['bic_cancel_qty'] ?? 0);
$codeTotal += $qty;
$codeCancelTotal += $cancelQty;
$isChecked = $cancelQty >= $qty && $qty > 0;
?>
<tr>
<td class="text-center">
<input type="checkbox" class="code-check"
<?= $bicIdx > 0 ? 'name="code_cancel_check[' . esc((string) $bicIdx, 'attr') . ']"' : '' ?>
value="1" <?= $isChecked ? 'checked' : '' ?>/>
</td>
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center"><?= esc((string) ($row['bic_bi2_idx'] ?? '')) ?></td>
<td class="pl-2"><?= esc((string) ($row['bic_issue_code'] ?? '')) ?></td>
<td class="text-right pr-2 code-qty"><?= number_format($qty) ?></td>
<td class="text-right pr-2">
<input type="number" min="0" max="<?= esc((string) $qty) ?>" class="code-cancel-input border border-gray-300 rounded px-1 py-0.5 w-24 text-right"
data-max="<?= esc((string) $qty) ?>"
data-bag-code="<?= esc($selectedBagCode, 'attr') ?>"
<?= $bicIdx > 0 ? 'name="code_cancel_qty[' . esc((string) $bicIdx, 'attr') . ']"' : '' ?>
value="<?= esc((string) $cancelQty) ?>"/>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="6" class="text-center text-gray-400 py-4">불출 품목 코드 내역이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-50 font-semibold">
<td colspan="4" class="text-center">합계</td>
<td class="text-right pr-2" id="code-total-qty"><?= number_format($codeTotal) ?></td>
<td class="text-right pr-2" id="code-total-cancel"><?= number_format($codeCancelTotal) ?></td>
</tr>
</tfoot>
</table>
</div>
</section>
</div>
</div>
<div class="mt-2 flex justify-end gap-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90">저장</button>
</div>
</form>
</div>
<script>
(() => {
const numberFormat = new Intl.NumberFormat('ko-KR');
const detailInputs = Array.from(document.querySelectorAll('.detail-cancel-input'));
const detailChecks = Array.from(document.querySelectorAll('.detail-check'));
const codeInputs = Array.from(document.querySelectorAll('.code-cancel-input'));
const codeChecks = Array.from(document.querySelectorAll('.code-check'));
const clamp = (value, max) => {
const n = parseInt(String(value || '0'), 10) || 0;
return Math.max(0, Math.min(max, n));
};
const syncDetailTotals = () => {
let qtySum = 0;
let cancelSum = 0;
document.querySelectorAll('#detail-table tbody tr').forEach((tr) => {
const qtyCell = tr.querySelector('.detail-qty');
const input = tr.querySelector('.detail-cancel-input');
if (!qtyCell || !input) return;
const qty = parseInt(qtyCell.textContent.replace(/,/g, ''), 10) || 0;
const cancel = clamp(input.value, parseInt(input.dataset.max || '0', 10) || 0);
qtySum += qty;
cancelSum += cancel;
});
const totalQty = document.getElementById('detail-total-qty');
const totalCancel = document.getElementById('detail-total-cancel');
if (totalQty) totalQty.textContent = numberFormat.format(qtySum);
if (totalCancel) totalCancel.textContent = numberFormat.format(cancelSum);
};
const syncCodeTotals = () => {
let qtySum = 0;
let cancelSum = 0;
document.querySelectorAll('#code-table tbody tr').forEach((tr) => {
const qtyCell = tr.querySelector('.code-qty');
if (!qtyCell) return;
const qty = parseInt(qtyCell.textContent.replace(/,/g, ''), 10) || 0;
qtySum += qty;
const input = tr.querySelector('.code-cancel-input');
if (input) {
cancelSum += clamp(input.value, parseInt(input.dataset.max || '0', 10) || 0);
}
});
const totalQty = document.getElementById('code-total-qty');
const totalCancel = document.getElementById('code-total-cancel');
if (totalQty) totalQty.textContent = numberFormat.format(qtySum);
if (totalCancel) totalCancel.textContent = numberFormat.format(cancelSum);
};
const syncSelectedBagCancelFromCodes = () => {
const codeInput = codeInputs[0];
if (!codeInput) return;
const bagCode = codeInput.dataset.bagCode || '';
if (bagCode === '') return;
let cancelSum = 0;
codeInputs.forEach((input) => {
const max = parseInt(input.dataset.max || '0', 10) || 0;
const value = clamp(input.value, max);
input.value = String(value);
cancelSum += value;
});
const detailInput = document.querySelector(`.detail-cancel-input[data-bag-code="${bagCode}"]`);
if (detailInput) {
const max = parseInt(detailInput.dataset.max || '0', 10) || 0;
detailInput.value = String(clamp(cancelSum, max));
const check = detailInput.closest('tr')?.querySelector('.detail-check');
if (check) {
check.checked = (clamp(cancelSum, max) >= max && max > 0);
}
}
};
detailInputs.forEach((input) => {
input.addEventListener('input', () => {
const max = parseInt(input.dataset.max || '0', 10) || 0;
input.value = String(clamp(input.value, max));
syncDetailTotals();
});
});
detailChecks.forEach((check) => {
check.addEventListener('change', () => {
const tr = check.closest('tr');
const input = tr?.querySelector('.detail-cancel-input');
if (!input) return;
const max = parseInt(input.dataset.max || '0', 10) || 0;
input.value = check.checked ? String(max) : '0';
const bagCode = input.dataset.bagCode || '';
if (bagCode !== '') {
codeInputs.forEach((codeInput) => {
if ((codeInput.dataset.bagCode || '') !== bagCode) return;
const codeMax = parseInt(codeInput.dataset.max || '0', 10) || 0;
codeInput.value = check.checked ? String(codeMax) : '0';
const codeCheck = codeInput.closest('tr')?.querySelector('.code-check');
if (codeCheck) {
codeCheck.checked = check.checked;
}
});
}
syncCodeTotals();
syncSelectedBagCancelFromCodes();
syncDetailTotals();
});
});
codeInputs.forEach((input) => {
input.addEventListener('input', () => {
const max = parseInt(input.dataset.max || '0', 10) || 0;
input.value = String(clamp(input.value, max));
const check = input.closest('tr')?.querySelector('.code-check');
if (check) {
check.checked = clamp(input.value, max) >= max && max > 0;
}
syncCodeTotals();
syncSelectedBagCancelFromCodes();
syncDetailTotals();
});
});
codeChecks.forEach((check) => {
check.addEventListener('change', () => {
const input = check.closest('tr')?.querySelector('.code-cancel-input');
if (!input) return;
const max = parseInt(input.dataset.max || '0', 10) || 0;
input.value = check.checked ? String(max) : '0';
syncCodeTotals();
syncSelectedBagCancelFromCodes();
syncDetailTotals();
});
});
syncCodeTotals();
syncSelectedBagCancelFromCodes();
syncDetailTotals();
})();
</script>