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:
taekyoungc
2026-07-13 12:26:44 +09:00
parent 03c2bbc05c
commit 65582c3439
6 changed files with 519 additions and 0 deletions

View 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>