feat: 맞춤 집계표 — 기간·품목구분·컬럼 선택 + 제목·계정별 프리셋 저장/인쇄
- 한 화면에서 기간·품목구분(봉투/음식물/폐기물)·집계방식·표시컬럼 선택, 제목 지정 후 조회/인쇄 - 컬럼 13종 선택, 상세/품목별 집계 지원, 실판매(bag_sale) 기준 - 계정별(mb_idx) 프리셋 저장/불러오기/삭제 (report_preset 테이블) - report_preset 테이블 없으면 프리셋만 비활성, 리포트는 정상 동작 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,9 +12,57 @@ use App\Models\PackagingUnitModel;
|
||||
use App\Models\DesignatedShopModel;
|
||||
use App\Models\LocalGovernmentModel;
|
||||
use App\Models\SalesAgencyModel;
|
||||
use App\Models\ReportPresetModel;
|
||||
|
||||
class SalesReport extends BaseController
|
||||
{
|
||||
/** 맞춤 집계표에서 선택 가능한 컬럼 카탈로그 (표시 순서 = 정의 순서) */
|
||||
private const CUSTOM_REPORT_COLUMNS = [
|
||||
'date' => ['label' => '일자', 'type' => 'str'],
|
||||
'shop_no' => ['label' => '지정번호', 'type' => 'str'],
|
||||
'shop_name' => ['label' => '판매소명', 'type' => 'str'],
|
||||
'rep_name' => ['label' => '대표자', 'type' => 'str'],
|
||||
'address' => ['label' => '소재지', 'type' => 'str'],
|
||||
'zone' => ['label' => '구역', 'type' => 'str'],
|
||||
'category' => ['label' => '품목구분', 'type' => 'str'],
|
||||
'product' => ['label' => '품목', 'type' => 'str'],
|
||||
'size' => ['label' => '용량', 'type' => 'str'],
|
||||
'qty' => ['label' => '판매량', 'type' => 'num'],
|
||||
'amount' => ['label' => '판매금액', 'type' => 'num'],
|
||||
'fee' => ['label' => '수수료', 'type' => 'num'],
|
||||
'total' => ['label' => '총액', 'type' => 'num'],
|
||||
];
|
||||
|
||||
private const CUSTOM_REPORT_DEFAULT_COLUMNS = ['date', 'shop_no', 'shop_name', 'product', 'qty', 'amount'];
|
||||
|
||||
/** 품목명 → 3분류(봉투/음식물/폐기물) */
|
||||
private function customReportCategory3(string $name): string
|
||||
{
|
||||
if (mb_strpos($name, '음식물') !== false) {
|
||||
return 'food';
|
||||
}
|
||||
if (mb_strpos($name, '폐기물') !== false || mb_strpos($name, '대형') !== false) {
|
||||
return 'waste';
|
||||
}
|
||||
return 'envelope';
|
||||
}
|
||||
|
||||
private function customReportCategoryLabel(string $cat): string
|
||||
{
|
||||
return ['all' => '전체', 'envelope' => '봉투', 'food' => '음식물', 'waste' => '폐기물'][$cat] ?? '전체';
|
||||
}
|
||||
|
||||
/** 품목명에서 용량 토큰 추출(예: 10L / 1000원) */
|
||||
private function customReportSize(string $name): string
|
||||
{
|
||||
if (preg_match('/(\d+)\s*[lL]/u', $name, $m) === 1) {
|
||||
return $m[1] . 'L';
|
||||
}
|
||||
if (preg_match('/([\d,]+)\s*원/u', $name, $m) === 1) {
|
||||
return str_replace(',', '', $m[1]) . '원';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* P5-01 / PWB-090101-001: 지정 판매소 판매 대장 (일자별·기간별, 엑셀·인쇄)
|
||||
*/
|
||||
@@ -3378,4 +3426,224 @@ class SalesReport extends BaseController
|
||||
'salesLabel' => $scopeLabel($salesScope),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 맞춤 집계표 — 기간·품목구분·컬럼을 골라 제목을 붙여 조회/인쇄, 계정별 프리셋 저장.
|
||||
*/
|
||||
public function customReport()
|
||||
{
|
||||
helper(['admin', 'export']);
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (! $lgIdx) {
|
||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
$mbIdx = (int) session()->get('mb_idx');
|
||||
|
||||
// report_preset 테이블이 있을 때만 프리셋 기능 활성(없으면 조회·인쇄는 그대로 동작)
|
||||
$presetsEnabled = \Config\Database::connect()->tableExists('report_preset');
|
||||
$presetModel = model(ReportPresetModel::class);
|
||||
$presets = [];
|
||||
if ($presetsEnabled) {
|
||||
$presets = $presetModel->where('rp_lg_idx', $lgIdx)->where('rp_mb_idx', $mbIdx)
|
||||
->orderBy('rp_name', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
// 활성 설정 결정: 폼 적용(applied) > 프리셋 로드(preset) > 기본값
|
||||
$presetIdx = (int) ($this->request->getGet('preset') ?? 0);
|
||||
$loaded = null;
|
||||
if ($presetsEnabled && $presetIdx > 0) {
|
||||
$loaded = $presetModel->find($presetIdx);
|
||||
if ($loaded === null || (int) $loaded->rp_lg_idx !== (int) $lgIdx || (int) $loaded->rp_mb_idx !== $mbIdx) {
|
||||
$loaded = null;
|
||||
}
|
||||
}
|
||||
|
||||
$catalogKeys = array_keys(self::CUSTOM_REPORT_COLUMNS);
|
||||
if ((string) ($this->request->getGet('applied') ?? '') === '1') {
|
||||
$cols = (array) ($this->request->getGet('cols') ?? []);
|
||||
$title = (string) ($this->request->getGet('title') ?? '');
|
||||
$category = (string) ($this->request->getGet('category') ?? 'all');
|
||||
$mode = (string) ($this->request->getGet('mode') ?? 'detail');
|
||||
} elseif ($loaded !== null) {
|
||||
$cols = json_decode((string) ($loaded->rp_columns ?? '[]'), true) ?: [];
|
||||
$title = (string) $loaded->rp_title;
|
||||
$category = (string) $loaded->rp_category;
|
||||
$mode = (string) $loaded->rp_mode;
|
||||
} else {
|
||||
$cols = self::CUSTOM_REPORT_DEFAULT_COLUMNS;
|
||||
$title = '';
|
||||
$category = 'all';
|
||||
$mode = 'detail';
|
||||
}
|
||||
// 정규화
|
||||
$selected = array_values(array_filter($catalogKeys, static fn ($k) => in_array($k, $cols, true)));
|
||||
if ($selected === []) {
|
||||
$selected = self::CUSTOM_REPORT_DEFAULT_COLUMNS;
|
||||
}
|
||||
if (! in_array($category, ['all', 'envelope', 'food', 'waste'], true)) {
|
||||
$category = 'all';
|
||||
}
|
||||
$mode = $mode === 'summary' ? 'summary' : 'detail';
|
||||
|
||||
$isYmd = static fn (string $d): bool => preg_match('/^\d{4}-\d{2}-\d{2}$/', $d) === 1;
|
||||
$startDate = (string) ($this->request->getGet('start_date') ?? date('Y-m-01'));
|
||||
$endDate = (string) ($this->request->getGet('end_date') ?? date('Y-m-d'));
|
||||
if (! $isYmd($startDate)) {
|
||||
$startDate = date('Y-m-01');
|
||||
}
|
||||
if (! $isYmd($endDate)) {
|
||||
$endDate = date('Y-m-d');
|
||||
}
|
||||
if ($startDate > $endDate) {
|
||||
[$startDate, $endDate] = [$endDate, $startDate];
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$hasBsFee = $db->fieldExists('bs_fee', 'bag_sale');
|
||||
$detail = $this->fetchSalesLedgerDetailRows($db, (int) $lgIdx, $startDate, $endDate, 0, 0, $hasBsFee);
|
||||
|
||||
// 카테고리 필터 + 행 매핑
|
||||
$rows = [];
|
||||
foreach ($detail as $r) {
|
||||
$name = (string) ($r->bs_bag_name ?? '');
|
||||
$cat3 = $this->customReportCategory3($name);
|
||||
if ($category !== 'all' && $cat3 !== $category) {
|
||||
continue;
|
||||
}
|
||||
$qty = (int) ($r->line_qty ?? 0);
|
||||
$amount = (float) ($r->line_amount ?? 0);
|
||||
$fee = (float) ($r->line_fee ?? 0);
|
||||
$rows[] = [
|
||||
'date' => substr((string) ($r->bs_sale_date ?? ''), 0, 10),
|
||||
'shop_no' => (string) ($r->ds_shop_no ?? ''),
|
||||
'shop_name' => (string) ($r->ds_name ?? ''),
|
||||
'rep_name' => (string) ($r->ds_rep_name ?? ''),
|
||||
'address' => (string) ($r->ds_addr ?? ''),
|
||||
'zone' => (string) ($r->ds_zone_code ?? ''),
|
||||
'category' => $this->customReportCategoryLabel($cat3),
|
||||
'product' => $name,
|
||||
'size' => $this->customReportSize($name),
|
||||
'qty' => $qty,
|
||||
'amount' => (int) round($amount),
|
||||
'fee' => (int) round($fee),
|
||||
'total' => (int) round($amount + $fee),
|
||||
];
|
||||
}
|
||||
|
||||
// 집계 모드: 품목(구분+품명+용량) 기준 합산
|
||||
if ($mode === 'summary') {
|
||||
$agg = [];
|
||||
foreach ($rows as $row) {
|
||||
$key = $row['category'] . '|' . $row['product'] . '|' . $row['size'];
|
||||
if (! isset($agg[$key])) {
|
||||
$agg[$key] = ['category' => $row['category'], 'product' => $row['product'], 'size' => $row['size'],
|
||||
'date' => '', 'shop_no' => '', 'shop_name' => '', 'rep_name' => '', 'address' => '', 'zone' => '',
|
||||
'qty' => 0, 'amount' => 0, 'fee' => 0, 'total' => 0];
|
||||
}
|
||||
$agg[$key]['qty'] += $row['qty'];
|
||||
$agg[$key]['amount'] += $row['amount'];
|
||||
$agg[$key]['fee'] += $row['fee'];
|
||||
$agg[$key]['total'] += $row['total'];
|
||||
}
|
||||
$rows = array_values($agg);
|
||||
}
|
||||
|
||||
// 숫자 컬럼 합계
|
||||
$totals = [];
|
||||
foreach ($selected as $key) {
|
||||
if ((self::CUSTOM_REPORT_COLUMNS[$key]['type'] ?? '') === 'num') {
|
||||
$totals[$key] = array_sum(array_map(static fn ($r) => (int) $r[$key], $rows));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderWorkPage('맞춤 집계표', 'admin/sales_report/custom_report', [
|
||||
'columnCatalog' => self::CUSTOM_REPORT_COLUMNS,
|
||||
'selected' => $selected,
|
||||
'title' => $title,
|
||||
'category' => $category,
|
||||
'mode' => $mode,
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
'rows' => $rows,
|
||||
'totals' => $totals,
|
||||
'presets' => $presets,
|
||||
'activePresetIdx' => $loaded !== null ? (int) $loaded->rp_idx : 0,
|
||||
'presetsEnabled' => $presetsEnabled,
|
||||
]);
|
||||
}
|
||||
|
||||
/** 맞춤 집계표 프리셋 저장(신규/수정) — 계정 소유 */
|
||||
public function customReportPresetSave()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
if (! $lgIdx) {
|
||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
$mbIdx = (int) session()->get('mb_idx');
|
||||
|
||||
if (! \Config\Database::connect()->tableExists('report_preset')) {
|
||||
return redirect()->to(site_url('bag/reports/custom'))
|
||||
->with('error', '프리셋 저장 테이블이 아직 생성되지 않았습니다. (report_preset_table.sql 실행 필요)');
|
||||
}
|
||||
$name = trim((string) $this->request->getPost('rp_name'));
|
||||
if ($name === '') {
|
||||
return redirect()->to(site_url('bag/reports/custom'))->with('error', '프리셋 이름을 입력해 주세요.');
|
||||
}
|
||||
$cols = (array) ($this->request->getPost('cols') ?? []);
|
||||
$catalog = array_keys(self::CUSTOM_REPORT_COLUMNS);
|
||||
$selected = array_values(array_filter($catalog, static fn ($k) => in_array($k, $cols, true)));
|
||||
if ($selected === []) {
|
||||
$selected = self::CUSTOM_REPORT_DEFAULT_COLUMNS;
|
||||
}
|
||||
$category = (string) $this->request->getPost('rp_category');
|
||||
if (! in_array($category, ['all', 'envelope', 'food', 'waste'], true)) {
|
||||
$category = 'all';
|
||||
}
|
||||
$mode = (string) $this->request->getPost('rp_mode') === 'summary' ? 'summary' : 'detail';
|
||||
|
||||
$data = [
|
||||
'rp_lg_idx' => $lgIdx,
|
||||
'rp_mb_idx' => $mbIdx,
|
||||
'rp_name' => mb_substr($name, 0, 100),
|
||||
'rp_title' => mb_substr((string) $this->request->getPost('rp_title'), 0, 200),
|
||||
'rp_category' => $category,
|
||||
'rp_mode' => $mode,
|
||||
'rp_columns' => json_encode($selected, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
|
||||
$presetModel = model(ReportPresetModel::class);
|
||||
$rpIdx = (int) ($this->request->getPost('rp_idx') ?? 0);
|
||||
if ($rpIdx > 0) {
|
||||
$existing = $presetModel->find($rpIdx);
|
||||
if ($existing !== null && (int) $existing->rp_lg_idx === (int) $lgIdx && (int) $existing->rp_mb_idx === $mbIdx) {
|
||||
$presetModel->update($rpIdx, $data);
|
||||
}
|
||||
} else {
|
||||
$presetModel->insert($data);
|
||||
$rpIdx = (int) $presetModel->getInsertID();
|
||||
}
|
||||
|
||||
$q = http_build_query(['preset' => $rpIdx, 'start_date' => $this->request->getPost('start_date'), 'end_date' => $this->request->getPost('end_date')]);
|
||||
|
||||
return redirect()->to(site_url('bag/reports/custom') . '?' . $q)->with('success', '프리셋을 저장했습니다.');
|
||||
}
|
||||
|
||||
/** 맞춤 집계표 프리셋 삭제 — 계정 소유 */
|
||||
public function customReportPresetDelete()
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = admin_effective_lg_idx();
|
||||
$mbIdx = (int) session()->get('mb_idx');
|
||||
$rpIdx = (int) ($this->request->getPost('rp_idx') ?? 0);
|
||||
$presetModel = model(ReportPresetModel::class);
|
||||
$existing = $rpIdx > 0 ? $presetModel->find($rpIdx) : null;
|
||||
if ($existing !== null && (int) $existing->rp_lg_idx === (int) $lgIdx && (int) $existing->rp_mb_idx === $mbIdx) {
|
||||
$presetModel->delete($rpIdx);
|
||||
|
||||
return redirect()->to(site_url('bag/reports/custom'))->with('success', '프리셋을 삭제했습니다.');
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('bag/reports/custom'))->with('error', '삭제할 프리셋을 찾을 수 없습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user