Files
jongryangje/app/Controllers/Admin/BagIssue.php
taekyoungc 0f1d414f37 사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.
통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 16:15:15 +09:00

367 lines
14 KiB
PHP

<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\BagIssueModel;
use App\Models\BagIssueItemCodeModel;
use App\Models\BagInventoryModel;
use App\Models\CodeKindModel;
use App\Models\CodeDetailModel;
use App\Models\FreeRecipientModel;
use App\Models\PackagingUnitModel;
class BagIssue extends BaseController
{
private BagIssueModel $issueModel;
private BagIssueItemCodeModel $issueItemCodeModel;
public function __construct()
{
$this->issueModel = model(BagIssueModel::class);
$this->issueItemCodeModel = model(BagIssueItemCodeModel::class);
}
/**
* 낱장 수량을 품목코드 단위로 분해한다.
*
* @return array<int,array{issueCode:string,qty:int}>
*/
private function buildIssueCodeRows(int $bi2Idx, int $sheetQty, array $packUnit): array
{
$sheetQty = max(0, $sheetQty);
if ($sheetQty <= 0) {
return [];
}
$chunkSize = max(
1,
(int) ($packUnit['totalPerBox'] ?? 0),
(int) ($packUnit['packPerSheet'] ?? 0)
);
$rows = [];
$remaining = $sheetQty;
$seq = 1;
while ($remaining > 0) {
$qty = min($chunkSize, $remaining);
$rows[] = [
'issueCode' => sprintf('%d-%06d-%03d', (int) date('y'), $bi2Idx, $seq),
'qty' => $qty,
];
$remaining -= $qty;
$seq++;
}
return $rows;
}
public function index()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
if (! $lgIdx) {
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
}
$builder = $this->issueModel->where('bi2_lg_idx', $lgIdx);
$startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date');
if ($startDate) {
$builder->where('bi2_issue_date >=', $startDate);
}
if ($endDate) {
$builder->where('bi2_issue_date <=', $endDate);
}
$list = $builder->orderBy('bi2_issue_date', 'DESC')->orderBy('bi2_idx', 'DESC')->paginate(20);
$pager = $this->issueModel->pager;
return $this->renderWorkPage('무료용 불출 관리', 'admin/bag_issue/index', compact('list', 'startDate', 'endDate', 'pager'));
}
public function create()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
return $this->renderWorkPage('무료용 불출 처리', 'admin/bag_issue/create', compact('bagCodes'));
}
public function store()
{
helper('admin');
$lgIdx = admin_effective_lg_idx();
$rules = [
'bi2_year' => 'required|is_natural_no_zero',
'bi2_quarter' => 'required|in_list[1,2,3,4]',
'bi2_issue_type' => 'required|max_length[20]',
'bi2_issue_date' => 'required|valid_date[Y-m-d]',
'bi2_dest_name' => 'required|max_length[100]',
// 사이트 다건 입력(item_bag_code/item_qty)과 기존 관리자 단건 입력을 함께 허용
'bi2_bag_code' => 'permit_empty|max_length[50]',
'bi2_qty' => 'permit_empty|is_natural_no_zero',
];
if (! $this->validate($rules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
}
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
$issueType = trim((string) $this->request->getPost('bi2_issue_type'));
$destType = trim((string) ($this->request->getPost('bi2_dest_type') ?? ''));
$destName = trim((string) ($this->request->getPost('bi2_dest_name') ?? ''));
$destDongCode = trim((string) ($this->request->getPost('bi2_dest_dong_code') ?? ''));
if ($destType === '') {
$destType = '동사무소';
}
if ($issueType === '공공용' && $destType === '동사무소') {
$destType = '구청';
}
if ($issueType === '무료용' && $destDongCode !== '') {
$existsFreeDong = model(FreeRecipientModel::class)
->where('fr_lg_idx', $lgIdx)
->where('fr_state', 1)
->where('fr_dong_code', $destDongCode)
->first();
if (! $existsFreeDong) {
return redirect()->back()->withInput()->with('error', '선택한 불출처는 무료용 대상이 아닙니다.');
}
}
$invRows = model(BagInventoryModel::class)
->where('bi_lg_idx', $lgIdx)
->where('bi_qty >', 0)
->findAll();
$inventoryMap = [];
foreach ($invRows as $inv) {
$code = (string) ($inv->bi_bag_code ?? '');
if ($code === '') {
continue;
}
$inventoryMap[$code] = [
'qty' => (int) ($inv->bi_qty ?? 0),
'name' => (string) ($inv->bi_bag_name ?? ''),
];
}
$unitRows = model(PackagingUnitModel::class)
->where('pu_lg_idx', $lgIdx)
->where('pu_state', 1)
->findAll();
$packMap = [];
foreach ($unitRows as $unit) {
$code = (string) ($unit->pu_bag_code ?? '');
if ($code === '') {
continue;
}
$packMap[$code] = [
'packPerSheet' => max(1, (int) ($unit->pu_pack_per_sheet ?? 1)),
'totalPerBox' => max(1, (int) ($unit->pu_total_per_box ?? 1)),
];
}
$items = [];
$itemCodes = $this->request->getPost('item_bag_code');
$itemQtys = $this->request->getPost('item_qty');
$itemPacks = $this->request->getPost('item_pack');
$itemCodes = is_array($itemCodes) ? $itemCodes : [];
$itemQtys = is_array($itemQtys) ? $itemQtys : [];
$itemPacks = is_array($itemPacks) ? $itemPacks : [];
$count = max(count($itemCodes), count($itemQtys), count($itemPacks));
for ($i = 0; $i < $count; $i++) {
$bagCode = trim((string) ($itemCodes[$i] ?? ''));
$qtyRaw = (int) ($itemQtys[$i] ?? 0);
$pack = trim((string) ($itemPacks[$i] ?? 'sheet'));
if ($bagCode === '' || $qtyRaw <= 0) {
continue;
}
if (! in_array($pack, ['box', 'pack', 'sheet'], true)) {
$pack = 'sheet';
}
$packUnit = $packMap[$bagCode] ?? ['packPerSheet' => 1, 'totalPerBox' => 1];
$sheetQty = $qtyRaw;
if ($pack === 'box') {
$sheetQty = $qtyRaw * (int) $packUnit['totalPerBox'];
} elseif ($pack === 'pack') {
$sheetQty = $qtyRaw * (int) $packUnit['packPerSheet'];
}
$sheetQty = max(1, (int) $sheetQty);
$detail = $kindO
? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $bagCode, $lgIdx)
: null;
$bagName = $detail ? (string) ($detail->cd_name ?? '') : (string) ($inventoryMap[$bagCode]['name'] ?? '');
if ($bagName === '') {
$bagName = (string) $bagCode;
}
$items[] = [
'bagCode' => $bagCode,
'bagName' => $bagName,
'pack' => $pack,
'rawQty' => $qtyRaw,
'sheetQty' => $sheetQty,
];
}
// 기존 관리자 단건 폼과의 호환
if ($items === []) {
$singleBagCode = trim((string) $this->request->getPost('bi2_bag_code'));
$singleQty = (int) $this->request->getPost('bi2_qty');
if ($singleBagCode !== '' && $singleQty > 0) {
$detail = $kindO
? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $singleBagCode, $lgIdx)
: null;
$bagName = $detail ? (string) ($detail->cd_name ?? '') : (string) ($inventoryMap[$singleBagCode]['name'] ?? '');
if ($bagName === '') {
$bagName = (string) $singleBagCode;
}
$items[] = [
'bagCode' => $singleBagCode,
'bagName' => $bagName,
'pack' => 'sheet',
'rawQty' => $singleQty,
'sheetQty' => $singleQty,
];
}
}
if ($items === []) {
return redirect()->back()->withInput()->with('error', '불출 품목을 1건 이상 입력해 주세요.');
}
$requiredByBag = [];
foreach ($items as $item) {
$code = (string) $item['bagCode'];
if (! isset($requiredByBag[$code])) {
$requiredByBag[$code] = 0;
}
$requiredByBag[$code] += (int) $item['sheetQty'];
}
foreach ($requiredByBag as $code => $requiredQty) {
$available = (int) ($inventoryMap[$code]['qty'] ?? 0);
if ($available <= 0) {
return redirect()->back()->withInput()->with('error', '입고 재고가 없는 봉투코드는 불출할 수 없습니다: ' . $code);
}
if ($available < $requiredQty) {
return redirect()->back()->withInput()->with('error', '재고가 부족합니다: ' . $code . ' (재고 ' . number_format($available) . ', 요청 ' . number_format($requiredQty) . ')');
}
}
$db = \Config\Database::connect();
$db->transStart();
$hasIssueCodeTable = $db->tableExists('bag_issue_item_code');
$issueYear = (int) $this->request->getPost('bi2_year');
$issueQuarter = (int) $this->request->getPost('bi2_quarter');
$issueDate = (string) $this->request->getPost('bi2_issue_date');
$createdCount = 0;
helper('audit');
foreach ($items as $item) {
$issueData = [
'bi2_lg_idx' => $lgIdx,
'bi2_year' => $issueYear,
'bi2_quarter' => $issueQuarter,
'bi2_issue_type' => $issueType,
'bi2_issue_date' => $issueDate,
'bi2_dest_type' => $destType,
'bi2_dest_name' => $destName,
'bi2_bag_code' => (string) $item['bagCode'],
'bi2_bag_name' => (string) $item['bagName'],
'bi2_qty' => (int) $item['sheetQty'],
'bi2_status' => 'normal',
'bi2_regdate' => date('Y-m-d H:i:s'),
];
$this->issueModel->insert($issueData);
$bi2Idx = (int) $this->issueModel->getInsertID();
audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx]));
if ($hasIssueCodeTable) {
$codeRows = $this->buildIssueCodeRows($bi2Idx, (int) $item['sheetQty'], $packMap[(string) $item['bagCode']] ?? []);
foreach ($codeRows as $codeRow) {
$this->issueItemCodeModel->insert([
'bic_lg_idx' => $lgIdx,
'bic_bi2_idx' => $bi2Idx,
'bic_bag_code' => (string) $item['bagCode'],
'bic_issue_code' => (string) $codeRow['issueCode'],
'bic_qty' => (int) $codeRow['qty'],
'bic_cancel_qty' => 0,
'bic_state' => 'normal',
'bic_regdate' => date('Y-m-d H:i:s'),
]);
}
}
model(BagInventoryModel::class)->adjustQty(
$lgIdx,
(string) $item['bagCode'],
(string) $item['bagName'],
-((int) $item['sheetQty'])
);
$createdCount++;
}
$db->transComplete();
if (! $db->transStatus()) {
return redirect()->back()->withInput()->with('error', '불출 처리 중 오류가 발생했습니다.');
}
return redirect()->to(mgmt_url('bag-issues'))->with('success', $createdCount . '건 불출 처리되었습니다.');
}
public function cancel(int $id)
{
helper('admin');
$item = $this->issueModel->find($id);
if (! $item || (int) $item->bi2_lg_idx !== admin_effective_lg_idx()) {
return redirect()->to(mgmt_url('bag-issues'))->with('error', '불출 내역을 찾을 수 없습니다.');
}
$db = \Config\Database::connect();
$db->transStart();
$hasIssueCodeTable = $db->tableExists('bag_issue_item_code');
$before = (array) $item;
$this->issueModel->update($id, ['bi2_status' => 'cancelled']);
helper('audit');
audit_log('update', 'bag_issue', $id, $before, ['bi2_status' => 'cancelled']);
$restoreQty = (int) $item->bi2_qty;
if ($hasIssueCodeTable) {
$codeRows = $db->table('bag_issue_item_code')
->select('bic_idx, bic_qty, bic_cancel_qty')
->where('bic_lg_idx', (int) $item->bi2_lg_idx)
->where('bic_bi2_idx', $id)
->get()
->getResultArray();
$restoreQty = 0;
foreach ($codeRows as $codeRow) {
$bicIdx = (int) ($codeRow['bic_idx'] ?? 0);
$qty = (int) ($codeRow['bic_qty'] ?? 0);
$oldCancel = (int) ($codeRow['bic_cancel_qty'] ?? 0);
$restoreQty += max(0, $qty - $oldCancel);
$db->table('bag_issue_item_code')
->where('bic_idx', $bicIdx)
->update([
'bic_cancel_qty' => $qty,
'bic_state' => 'cancelled',
]);
}
}
model(BagInventoryModel::class)->adjustQty((int) $item->bi2_lg_idx, $item->bi2_bag_code, $item->bi2_bag_name, $restoreQty);
$this->issueModel->update($id, ['bi2_qty' => 0, 'bi2_status' => 'cancelled']);
$db->transComplete();
return redirect()->to(mgmt_url('bag-issues'))->with('success', '불출이 취소되었습니다.');
}
}