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:
@@ -211,6 +211,9 @@ $routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void
|
|||||||
$routes->post('packaging-units/manage/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
|
$routes->post('packaging-units/manage/delete/(:num)', 'Admin\PackagingUnit::delete/$1');
|
||||||
$routes->get('packaging-units/manage/history/(:num)', 'Admin\PackagingUnit::history/$1');
|
$routes->get('packaging-units/manage/history/(:num)', 'Admin\PackagingUnit::history/$1');
|
||||||
|
|
||||||
|
$routes->get('reports/custom', 'Admin\SalesReport::customReport');
|
||||||
|
$routes->post('reports/custom/preset/save', 'Admin\SalesReport::customReportPresetSave');
|
||||||
|
$routes->post('reports/custom/preset/delete', 'Admin\SalesReport::customReportPresetDelete');
|
||||||
$routes->get('reports/sales-ledger', 'Admin\SalesReport::salesLedger');
|
$routes->get('reports/sales-ledger', 'Admin\SalesReport::salesLedger');
|
||||||
$routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary');
|
$routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary');
|
||||||
$routes->get('reports/period-sales', 'Admin\SalesReport::periodSales');
|
$routes->get('reports/period-sales', 'Admin\SalesReport::periodSales');
|
||||||
|
|||||||
@@ -12,9 +12,57 @@ use App\Models\PackagingUnitModel;
|
|||||||
use App\Models\DesignatedShopModel;
|
use App\Models\DesignatedShopModel;
|
||||||
use App\Models\LocalGovernmentModel;
|
use App\Models\LocalGovernmentModel;
|
||||||
use App\Models\SalesAgencyModel;
|
use App\Models\SalesAgencyModel;
|
||||||
|
use App\Models\ReportPresetModel;
|
||||||
|
|
||||||
class SalesReport extends BaseController
|
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: 지정 판매소 판매 대장 (일자별·기간별, 엑셀·인쇄)
|
* P5-01 / PWB-090101-001: 지정 판매소 판매 대장 (일자별·기간별, 엑셀·인쇄)
|
||||||
*/
|
*/
|
||||||
@@ -3378,4 +3426,224 @@ class SalesReport extends BaseController
|
|||||||
'salesLabel' => $scopeLabel($salesScope),
|
'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', '삭제할 프리셋을 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/Models/ReportPresetModel.php
Normal file
21
app/Models/ReportPresetModel.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class ReportPresetModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'report_preset';
|
||||||
|
protected $primaryKey = 'rp_idx';
|
||||||
|
protected $returnType = 'object';
|
||||||
|
protected $useTimestamps = true;
|
||||||
|
protected $createdField = 'rp_regdate';
|
||||||
|
protected $updatedField = 'rp_updated';
|
||||||
|
protected $allowedFields = [
|
||||||
|
'rp_lg_idx', 'rp_mb_idx', 'rp_name', 'rp_title',
|
||||||
|
'rp_category', 'rp_mode', 'rp_columns',
|
||||||
|
];
|
||||||
|
}
|
||||||
188
app/Views/admin/sales_report/custom_report.php
Normal file
188
app/Views/admin/sales_report/custom_report.php
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label:string,type:string}> $columnCatalog
|
||||||
|
* @var list<string> $selected
|
||||||
|
* @var string $title
|
||||||
|
* @var string $category
|
||||||
|
* @var string $mode
|
||||||
|
* @var string $startDate
|
||||||
|
* @var string $endDate
|
||||||
|
* @var list<array<string,mixed>> $rows
|
||||||
|
* @var array<string,int> $totals
|
||||||
|
* @var list<object> $presets
|
||||||
|
* @var int $activePresetIdx
|
||||||
|
*/
|
||||||
|
$catLabels = ['all' => '전체', 'envelope' => '봉투', 'food' => '음식물', 'waste' => '폐기물'];
|
||||||
|
$printTitle = $title !== '' ? $title : '맞춤 집계표';
|
||||||
|
$nf = static fn ($n): string => number_format((int) $n);
|
||||||
|
?>
|
||||||
|
<?= view('components/print_header', [
|
||||||
|
'printTitle' => $printTitle,
|
||||||
|
'printDate' => date('Y-m-d'),
|
||||||
|
'printExtraLines' => ['조회기간: ' . $startDate . ' ~ ' . $endDate . ' · 품목구분: ' . ($catLabels[$category] ?? '전체') . ' · (단위: 매 / 원)'],
|
||||||
|
]) ?>
|
||||||
|
|
||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<span class="text-sm font-bold text-gray-700">맞춤 집계표</span>
|
||||||
|
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 조회/구성 폼 -->
|
||||||
|
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
||||||
|
<form method="get" action="<?= mgmt_url('reports/custom') ?>" id="custom-report-form" class="space-y-3">
|
||||||
|
<input type="hidden" name="applied" value="1"/>
|
||||||
|
<div class="flex flex-wrap items-end gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">표 제목</label>
|
||||||
|
<input type="text" name="title" value="<?= esc($title, 'attr') ?>" maxlength="200" placeholder="예: 2026년 음식물 봉투 판매 집계" class="border border-gray-300 rounded px-2 py-1 w-72"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">시작일</label>
|
||||||
|
<input type="date" name="start_date" value="<?= esc($startDate, 'attr') ?>" class="border border-gray-300 rounded px-2 py-1"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">종료일</label>
|
||||||
|
<input type="date" name="end_date" value="<?= esc($endDate, 'attr') ?>" class="border border-gray-300 rounded px-2 py-1"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">품목구분(종류)</label>
|
||||||
|
<select name="category" class="border border-gray-300 rounded px-2 py-1 w-32">
|
||||||
|
<?php foreach ($catLabels as $ck => $cl): ?>
|
||||||
|
<option value="<?= esc($ck, 'attr') ?>" <?= $category === $ck ? 'selected' : '' ?>><?= esc($cl) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">집계 방식</label>
|
||||||
|
<select name="mode" class="border border-gray-300 rounded px-2 py-1 w-40">
|
||||||
|
<option value="detail" <?= $mode === 'detail' ? 'selected' : '' ?>>상세(건별)</option>
|
||||||
|
<option value="summary" <?= $mode === 'summary' ? 'selected' : '' ?>>품목별 집계</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">조회</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">표시할 컬럼 선택</label>
|
||||||
|
<div class="flex flex-wrap gap-x-4 gap-y-1">
|
||||||
|
<?php foreach ($columnCatalog as $key => $meta): ?>
|
||||||
|
<label class="inline-flex items-center gap-1 text-sm cursor-pointer">
|
||||||
|
<input type="checkbox" name="cols[]" value="<?= esc($key, 'attr') ?>" <?= in_array($key, $selected, true) ? 'checked' : '' ?>/>
|
||||||
|
<span><?= esc($meta['label']) ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-gray-500 mt-1">집계 방식이 「품목별 집계」이면 일자·판매소 등 건별 컬럼은 빈 값으로 표시됩니다.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 프리셋 (계정별 저장) -->
|
||||||
|
<?php if (! empty($presetsEnabled)): ?>
|
||||||
|
<section class="p-3 bg-gray-50 border-b border-gray-200 no-print">
|
||||||
|
<div class="flex flex-wrap items-end gap-3 text-sm">
|
||||||
|
<form method="get" action="<?= mgmt_url('reports/custom') ?>" class="flex items-end gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">저장된 프리셋 불러오기</label>
|
||||||
|
<select name="preset" class="border border-gray-300 rounded px-2 py-1 min-w-[14rem]" onchange="this.form.submit()">
|
||||||
|
<option value="">— 프리셋 선택 —</option>
|
||||||
|
<?php foreach ($presets as $p): ?>
|
||||||
|
<option value="<?= (int) $p->rp_idx ?>" <?= $activePresetIdx === (int) $p->rp_idx ? 'selected' : '' ?>><?= esc((string) $p->rp_name) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="start_date" value="<?= esc($startDate, 'attr') ?>"/>
|
||||||
|
<input type="hidden" name="end_date" value="<?= esc($endDate, 'attr') ?>"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php if ($activePresetIdx > 0): ?>
|
||||||
|
<form method="post" action="<?= mgmt_url('reports/custom/preset/delete') ?>" onsubmit="return confirm('이 프리셋을 삭제할까요?');" class="flex items-end">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="rp_idx" value="<?= (int) $activePresetIdx ?>"/>
|
||||||
|
<button type="submit" class="text-red-600 hover:underline text-xs pb-1">현재 프리셋 삭제</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 현재 화면 구성 그대로 저장 (JS로 현재 폼 값 복사) -->
|
||||||
|
<form method="post" action="<?= mgmt_url('reports/custom/preset/save') ?>" id="preset-save-form" class="flex items-end gap-2">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="rp_idx" value="<?= (int) $activePresetIdx ?>"/>
|
||||||
|
<input type="hidden" name="rp_title" id="ps-title"/>
|
||||||
|
<input type="hidden" name="rp_category" id="ps-category"/>
|
||||||
|
<input type="hidden" name="rp_mode" id="ps-mode"/>
|
||||||
|
<input type="hidden" name="start_date" value="<?= esc($startDate, 'attr') ?>"/>
|
||||||
|
<input type="hidden" name="end_date" value="<?= esc($endDate, 'attr') ?>"/>
|
||||||
|
<div id="ps-cols-holder"></div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">프리셋 이름</label>
|
||||||
|
<input type="text" name="rp_name" maxlength="100" placeholder="예: 음식물 담당 월집계" class="border border-gray-300 rounded px-2 py-1 w-56" required/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="bg-[#243a5e] text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">현재 구성 저장</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 결과 표 -->
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="mb-2 text-center no-print">
|
||||||
|
<h1 class="text-lg font-bold m-0"><?= esc($printTitle) ?></h1>
|
||||||
|
<p class="text-xs text-gray-500 mt-0.5">조회기간 <?= esc($startDate) ?> ~ <?= esc($endDate) ?> · 품목구분 <?= esc($catLabels[$category] ?? '전체') ?> · 건수 <?= $nf(count($rows)) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<table class="w-full data-table text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-12 text-center">No</th>
|
||||||
|
<?php foreach ($selected as $key): ?>
|
||||||
|
<th class="<?= ($columnCatalog[$key]['type'] ?? '') === 'num' ? 'text-right' : 'text-left' ?> px-2"><?= esc($columnCatalog[$key]['label']) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($rows === []): ?>
|
||||||
|
<tr><td colspan="<?= count($selected) + 1 ?>" class="text-center text-gray-400 py-6">조회된 데이터가 없습니다.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($rows as $i => $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center text-gray-500"><?= $i + 1 ?></td>
|
||||||
|
<?php foreach ($selected as $key): ?>
|
||||||
|
<?php $isNum = ($columnCatalog[$key]['type'] ?? '') === 'num'; ?>
|
||||||
|
<td class="<?= $isNum ? 'text-right' : 'text-left' ?> px-2"><?= $isNum ? $nf($row[$key] ?? 0) : esc((string) ($row[$key] ?? '')) ?></td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<tr class="bg-amber-50 font-bold">
|
||||||
|
<td class="text-center">합계</td>
|
||||||
|
<?php foreach ($selected as $key): ?>
|
||||||
|
<?php $isNum = ($columnCatalog[$key]['type'] ?? '') === 'num'; ?>
|
||||||
|
<td class="<?= $isNum ? 'text-right' : '' ?> px-2"><?= $isNum ? $nf($totals[$key] ?? 0) : '' ?></td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
// "현재 구성 저장" 시, 조회 폼의 현재 값(제목/종류/방식/컬럼)을 프리셋 저장 폼으로 복사
|
||||||
|
var mainForm = document.getElementById('custom-report-form');
|
||||||
|
var saveForm = document.getElementById('preset-save-form');
|
||||||
|
if (!mainForm || !saveForm) return;
|
||||||
|
saveForm.addEventListener('submit', function () {
|
||||||
|
document.getElementById('ps-title').value = mainForm.querySelector('[name="title"]').value;
|
||||||
|
document.getElementById('ps-category').value = mainForm.querySelector('[name="category"]').value;
|
||||||
|
document.getElementById('ps-mode').value = mainForm.querySelector('[name="mode"]').value;
|
||||||
|
var holder = document.getElementById('ps-cols-holder');
|
||||||
|
holder.innerHTML = '';
|
||||||
|
mainForm.querySelectorAll('input[name="cols[]"]:checked').forEach(function (cb) {
|
||||||
|
var h = document.createElement('input');
|
||||||
|
h.type = 'hidden'; h.name = 'cols[]'; h.value = cb.value;
|
||||||
|
holder.appendChild(h);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
21
writable/database/menu_add_custom_report.sql
Normal file
21
writable/database/menu_add_custom_report.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
-- 판매 현황 카테고리에 '맞춤 집계표' 소메뉴 추가 (사이트 메뉴)
|
||||||
|
-- 실행: mysql -u jongryangje -p jongryangje_dev < writable/database/menu_add_custom_report.sql
|
||||||
|
-- 멱등: 이미 있으면 건너뜀.
|
||||||
|
|
||||||
|
SET @mt_site := (SELECT mt_idx FROM menu_type WHERE mt_code = 'site' LIMIT 1);
|
||||||
|
SET @parent_sales := (
|
||||||
|
SELECT mm_idx FROM menu
|
||||||
|
WHERE mt_idx = @mt_site AND lg_idx = 1 AND mm_pidx = 0 AND mm_name = '판매 현황'
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
SET @next_num := (
|
||||||
|
SELECT IFNULL(MAX(mm_num), -1) + 1 FROM menu WHERE mt_idx = @mt_site AND lg_idx = 1 AND mm_pidx = @parent_sales
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO menu (mt_idx, lg_idx, mm_name, mm_link, mm_pidx, mm_dep, mm_num, mm_cnode, mm_level, mm_is_view)
|
||||||
|
SELECT @mt_site, 1, '맞춤 집계표', 'bag/reports/custom', @parent_sales, 1, @next_num, 0, '', 'Y'
|
||||||
|
WHERE @parent_sales IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM menu m
|
||||||
|
WHERE m.mt_idx = @mt_site AND m.lg_idx = 1 AND m.mm_pidx = @parent_sales AND m.mm_name = '맞춤 집계표'
|
||||||
|
);
|
||||||
18
writable/database/report_preset_table.sql
Normal file
18
writable/database/report_preset_table.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-- 맞춤 집계표: 계정별 리포트 프리셋(제목·품목구분·컬럼구성) 저장
|
||||||
|
-- 실행: mysql -u jongryangje -p jongryangje_dev < writable/database/report_preset_table.sql
|
||||||
|
-- 멱등(IF NOT EXISTS).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `report_preset` (
|
||||||
|
`rp_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`rp_lg_idx` INT UNSIGNED NOT NULL COMMENT '지자체',
|
||||||
|
`rp_mb_idx` INT UNSIGNED NOT NULL COMMENT '소유 계정(mb_idx) — 계정별 저장',
|
||||||
|
`rp_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '프리셋 이름',
|
||||||
|
`rp_title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '리포트(표) 제목',
|
||||||
|
`rp_category` VARCHAR(20) NOT NULL DEFAULT 'all' COMMENT 'all|envelope|food|waste',
|
||||||
|
`rp_mode` VARCHAR(10) NOT NULL DEFAULT 'detail' COMMENT 'detail|summary',
|
||||||
|
`rp_columns` TEXT NULL COMMENT '선택 컬럼 키 JSON 배열',
|
||||||
|
`rp_regdate` DATETIME NULL,
|
||||||
|
`rp_updated` DATETIME NULL,
|
||||||
|
PRIMARY KEY (`rp_idx`),
|
||||||
|
KEY `idx_rp_owner` (`rp_lg_idx`, `rp_mb_idx`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
Reference in New Issue
Block a user