지정판매소 신규/취소 현황을 사용자 지자체 기준으로 고정 조회하도록 정리하고, 동별 요약과 컬럼 설명 툴팁을 추가했습니다. 또한 지정판매소 바코드 출력 메뉴를 전용 URL로 분리하고 선택 인쇄/출력 레이아웃을 GBMS 형태에 맞춰 구현했습니다.
388 lines
16 KiB
PHP
388 lines
16 KiB
PHP
<?php
|
||
$ry = (int) ($reportYear ?? (int) date('Y'));
|
||
$exportUrl = mgmt_url('designated-shops/status/export') . '?' . http_build_query([
|
||
'year' => $ry,
|
||
]);
|
||
$fixedGugunLabel = trim((string) ($fixedGugunLabel ?? ''));
|
||
$regionColLabel = '군·구';
|
||
$sumCurrForPct = (int) ($districtTotal->curr_end ?? 0);
|
||
?>
|
||
<style>
|
||
.ds-status-x-scroll {
|
||
overflow-x: auto;
|
||
overflow-y: visible;
|
||
-webkit-overflow-scrolling: touch;
|
||
scroll-behavior: smooth;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
background: #fff;
|
||
max-width: 100%;
|
||
}
|
||
@media print {
|
||
.ds-status-x-scroll { overflow: visible !important; border: none; }
|
||
}
|
||
.ds-status-x-scroll .ds-status-table {
|
||
width: max-content;
|
||
min-width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
.ds-status-x-scroll .ds-status-table th,
|
||
.ds-status-x-scroll .ds-status-table td {
|
||
white-space: nowrap;
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
}
|
||
.ds-status-x-scroll .ds-status-table thead th {
|
||
background: #e9ecef;
|
||
border: 1px solid #ccc;
|
||
}
|
||
.ds-status-x-scroll .ds-status-table tbody td {
|
||
border: 1px solid #ccc;
|
||
}
|
||
.ds-status-x-scroll th.sticky-num,
|
||
.ds-status-x-scroll td.sticky-num {
|
||
position: sticky;
|
||
left: 0;
|
||
z-index: 3;
|
||
min-width: 3rem;
|
||
max-width: 3rem;
|
||
width: 3rem;
|
||
box-sizing: border-box;
|
||
background: #e9ecef;
|
||
border-right: 1px solid #bbb;
|
||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.06);
|
||
}
|
||
.ds-status-x-scroll td.sticky-num {
|
||
background: #fff;
|
||
text-align: center;
|
||
}
|
||
.ds-status-x-scroll tr.sum-row td.sticky-num {
|
||
background: #f3f4f6;
|
||
}
|
||
.ds-status-x-scroll th.sticky-region,
|
||
.ds-status-x-scroll td.sticky-region {
|
||
position: sticky;
|
||
left: 3rem;
|
||
z-index: 2;
|
||
background: #e9ecef;
|
||
border-right: 1px solid #bbb;
|
||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.06);
|
||
max-width: 16rem;
|
||
text-align: left;
|
||
}
|
||
.ds-status-x-scroll td.sticky-region {
|
||
background: #fff;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.ds-status-x-scroll tr.sum-row td.sticky-region {
|
||
background: #f3f4f6;
|
||
}
|
||
.ds-help {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.ds-help-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 1rem;
|
||
height: 1rem;
|
||
padding: 0 2px;
|
||
border: 1px solid #64748b;
|
||
border-radius: 999px;
|
||
background: #fef9c3;
|
||
color: #111827;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
font-family: Arial, sans-serif;
|
||
cursor: help;
|
||
user-select: none;
|
||
}
|
||
.ds-floating-tip {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
display: none;
|
||
max-width: min(22rem, calc(100vw - 16px));
|
||
padding: 6px 8px;
|
||
border: 1px solid #8fa0b3;
|
||
border-radius: 4px;
|
||
background: #1f2937;
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
line-height: 1.35;
|
||
text-align: left;
|
||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||
z-index: 9999;
|
||
pointer-events: none;
|
||
}
|
||
</style>
|
||
<?= view('components/print_header', ['printTitle' => '지정판매소 신규·취소 현황 (' . $ry . '년)']) ?>
|
||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
|
||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||
<span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span>
|
||
<div class="flex items-center gap-2">
|
||
<a href="<?= esc($exportUrl) ?>" class="border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||
<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>
|
||
<a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||
<form method="get" action="<?= mgmt_url('designated-shops/status') ?>" class="flex flex-wrap items-end gap-3">
|
||
<div>
|
||
<label class="block text-xs text-gray-600 mb-0.5">연도</label>
|
||
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[6rem]">
|
||
<?php foreach (($yearChoices ?? []) as $y): ?>
|
||
<option value="<?= (int) $y ?>" <?= $ry === (int) $y ? 'selected' : '' ?>><?= (int) $y ?>년</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="min-w-[12rem]">
|
||
<label class="block text-xs text-gray-600 mb-0.5">군·구</label>
|
||
<div class="border border-gray-300 rounded px-3 py-1 text-sm bg-gray-50 text-gray-800 font-medium pointer-events-none select-none">
|
||
<?= esc($fixedGugunLabel !== '' ? $fixedGugunLabel : '현재 지자체 기준') ?>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||
</form>
|
||
<p class="text-xs text-gray-500 mt-2">
|
||
종전·현행은 각각 <?= $ry - 1 ?>-12-31·<?= $ry ?>-12-31 기준 정상(지정일 이전·비정상 전환일 이후) 건수이며, 지정·취소는 <?= $ry ?>년 달력연도 기준입니다. 군·구는 현재 로그인 사용자의 지자체 기준으로 고정 표시됩니다.
|
||
</p>
|
||
</section>
|
||
|
||
<!-- 인쇄 시에도 보이는 본표 -->
|
||
<div class="mx-2 mt-2 mb-2 ds-status-x-scroll">
|
||
<table class="ds-status-table data-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="sticky-num text-center w-12">순번</th>
|
||
<th class="sticky-region"><?= esc($regionColLabel) ?></th>
|
||
<th class="text-left">
|
||
<span class="ds-help">구코드 <span class="ds-help-badge" tabindex="0" data-tip="지정판매소에 저장된 구·군 코드 값">?</span></span>
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">종전 <span class="ds-help-badge" tabindex="0" data-tip="전년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(전년도말)
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">지정 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 지정일이 속한 신규 지정 건수">?</span></span>(<?= $ry ?>년)
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">취소 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 폐업/해지 전환일이 속한 건수">?</span></span>(<?= $ry ?>년)
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">현행 <span class="ds-help-badge" tabindex="0" data-tip="조회년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(금년도말)
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">증감 <span class="ds-help-badge" tabindex="0" data-tip="현행에서 종전을 뺀 값 (현행−종전)">?</span></span>
|
||
<br/><span class="font-normal text-xs">(현행−종전)</span>
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">지정−취소 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 지정 건수에서 취소 건수를 뺀 값">?</span></span>
|
||
<br/><span class="font-normal text-xs">(<?= $ry ?>년)</span>
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">현행비중 <span class="ds-help-badge" tabindex="0" data-tip="전체 현행 합계 대비 해당 행 현행 건수의 비율(%)">?</span></span>
|
||
<br/><span class="font-normal text-xs">(%)</span>
|
||
</th>
|
||
<th class="text-right">
|
||
<span class="ds-help">전년대비 <span class="ds-help-badge ds-help-right" tabindex="0" data-tip="((현행−종전) / 종전) × 100, 종전이 0이면 표시 안함">?</span></span>
|
||
<br/><span class="font-normal text-xs">증감률(%)</span>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="text-right">
|
||
<?php $rowNo = 0; ?>
|
||
<?php foreach (($districtRows ?? []) as $row): ?>
|
||
<?php
|
||
$rowNo++;
|
||
$curr = (int) $row->curr_end;
|
||
$prev = (int) $row->prev_end;
|
||
$pctShare = $sumCurrForPct > 0 ? round(($curr / $sumCurrForPct) * 100, 1) : 0.0;
|
||
$yoyPct = $prev > 0 ? round((($curr - $prev) / $prev) * 100, 1) : null;
|
||
?>
|
||
<tr>
|
||
<td class="sticky-num"><?= $rowNo ?></td>
|
||
<td class="sticky-region" title="<?= esc($row->region_label) ?>"><?= esc($row->region_label) ?></td>
|
||
<td class="text-left text-xs"><?= esc((string) ($row->gugun_code ?? '')) ?></td>
|
||
<td><?= number_format($prev) ?></td>
|
||
<td><?= number_format((int) $row->designated_y) ?></td>
|
||
<td><?= number_format((int) $row->cancelled_y) ?></td>
|
||
<td><?= number_format($curr) ?></td>
|
||
<td><?= number_format((int) ($row->delta_curr_prev ?? 0)) ?></td>
|
||
<td><?= number_format((int) ($row->delta_des_cancel ?? 0)) ?></td>
|
||
<td><?= $pctShare ?></td>
|
||
<td><?= $yoyPct !== null ? $yoyPct : '—' ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($districtRows)): ?>
|
||
<tr><td colspan="11" class="text-center text-gray-400 py-6">조건에 맞는 데이터가 없습니다.</td></tr>
|
||
<?php endif; ?>
|
||
<?php if (! empty($districtRows) && isset($districtTotal)): ?>
|
||
<tr class="font-bold bg-gray-50 sum-row">
|
||
<td class="sticky-num">—</td>
|
||
<td class="sticky-region"><?= esc($districtTotal->region_label) ?></td>
|
||
<td class="text-left">—</td>
|
||
<td><?= number_format((int) $districtTotal->prev_end) ?></td>
|
||
<td><?= number_format((int) $districtTotal->designated_y) ?></td>
|
||
<td><?= number_format((int) $districtTotal->cancelled_y) ?></td>
|
||
<td><?= number_format((int) $districtTotal->curr_end) ?></td>
|
||
<td><?= number_format((int) ($districtTotal->delta_curr_prev ?? 0)) ?></td>
|
||
<td><?= number_format((int) ($districtTotal->delta_des_cancel ?? 0)) ?></td>
|
||
<td>100</td>
|
||
<td>
|
||
<?php
|
||
$tPrev = (int) $districtTotal->prev_end;
|
||
$tCurr = (int) $districtTotal->curr_end;
|
||
echo $tPrev > 0 ? round((($tCurr - $tPrev) / $tPrev) * 100, 1) : '—';
|
||
?>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<?php $zoneRows = $zoneSummaryRows ?? []; ?>
|
||
<section class="mx-2 mb-3 no-print">
|
||
<div class="text-xs font-semibold text-gray-700 mb-1">동별 현행 요약 (<?= esc($fixedGugunLabel ?? '군·구') ?>)</div>
|
||
<?php if (! empty($zoneRows)): ?>
|
||
<div class="flex flex-wrap gap-1 mb-2">
|
||
<?php foreach ($zoneRows as $z): ?>
|
||
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded border border-gray-300 bg-gray-50 text-gray-700">
|
||
<?= esc((string) $z->zone_label) ?> <?= number_format((int) $z->curr_end) ?>
|
||
</span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="border border-gray-300 bg-white overflow-auto max-h-56">
|
||
<table class="w-full data-table text-xs">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">동</th>
|
||
<th class="text-right">종전</th>
|
||
<th class="text-right">지정</th>
|
||
<th class="text-right">취소</th>
|
||
<th class="text-right">현행</th>
|
||
<th class="text-right">증감</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="text-right">
|
||
<?php foreach ($zoneRows as $z): ?>
|
||
<tr>
|
||
<td class="text-left"><?= esc((string) $z->zone_label) ?></td>
|
||
<td><?= number_format((int) $z->prev_end) ?></td>
|
||
<td><?= number_format((int) $z->designated_y) ?></td>
|
||
<td><?= number_format((int) $z->cancelled_y) ?></td>
|
||
<td><?= number_format((int) $z->curr_end) ?></td>
|
||
<td><?= number_format((int) $z->delta_curr_prev) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php else: ?>
|
||
<p class="text-xs text-gray-500">동별 집계 데이터가 없습니다.</p>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<div id="ds-floating-tip" class="ds-floating-tip no-print" aria-hidden="true"></div>
|
||
|
||
<script>
|
||
(function () {
|
||
var tipEl = document.getElementById('ds-floating-tip');
|
||
if (!tipEl) return;
|
||
var badges = Array.prototype.slice.call(document.querySelectorAll('.ds-help-badge'));
|
||
if (!badges.length) return;
|
||
|
||
function placeTip(target) {
|
||
var text = String(target.getAttribute('data-tip') || '').trim();
|
||
if (!text) return;
|
||
tipEl.textContent = text;
|
||
tipEl.style.display = 'block';
|
||
tipEl.setAttribute('aria-hidden', 'false');
|
||
|
||
var rect = target.getBoundingClientRect();
|
||
var tipRect = tipEl.getBoundingClientRect();
|
||
var vw = window.innerWidth || document.documentElement.clientWidth;
|
||
var vh = window.innerHeight || document.documentElement.clientHeight;
|
||
var gap = 8;
|
||
|
||
var left = rect.left + (rect.width / 2) - (tipRect.width / 2);
|
||
var top = rect.bottom + gap;
|
||
|
||
if (left < gap) left = gap;
|
||
if (left + tipRect.width > vw - gap) left = vw - gap - tipRect.width;
|
||
if (top + tipRect.height > vh - gap) top = rect.top - gap - tipRect.height;
|
||
if (top < gap) top = gap;
|
||
|
||
tipEl.style.left = Math.round(left) + 'px';
|
||
tipEl.style.top = Math.round(top) + 'px';
|
||
}
|
||
|
||
function hideTip() {
|
||
tipEl.style.display = 'none';
|
||
tipEl.setAttribute('aria-hidden', 'true');
|
||
tipEl.textContent = '';
|
||
}
|
||
|
||
badges.forEach(function (badge) {
|
||
badge.addEventListener('mouseenter', function () { placeTip(badge); });
|
||
badge.addEventListener('focus', function () { placeTip(badge); });
|
||
badge.addEventListener('mouseleave', hideTip);
|
||
badge.addEventListener('blur', hideTip);
|
||
});
|
||
window.addEventListener('scroll', hideTip, true);
|
||
window.addEventListener('resize', hideTip);
|
||
})();
|
||
</script>
|
||
|
||
<details class="mx-2 mb-4 no-print text-sm">
|
||
<summary class="cursor-pointer text-gray-600 hover:text-gray-800">연도별 요약 (참고)</summary>
|
||
<div class="flex gap-4 mt-2">
|
||
<div class="border border-gray-300 p-2 flex-1">
|
||
<div class="text-xs font-bold text-gray-700 mb-1">활성 / 비활성 / 전체</div>
|
||
<div class="text-sm">활성 <?= number_format((int) ($totalActive ?? 0)) ?> · 비활성 <?= number_format((int) ($totalInactive ?? 0)) ?> · 합 <?= number_format((int) ($totalActive ?? 0) + (int) ($totalInactive ?? 0)) ?></div>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||
<div>
|
||
<h3 class="text-xs font-bold text-gray-700 mb-1">연도별 신규등록 (지정일)</h3>
|
||
<div class="border border-gray-300 overflow-auto max-h-48">
|
||
<table class="w-full data-table text-xs">
|
||
<thead><tr><th>연도</th><th>건수</th></tr></thead>
|
||
<tbody class="text-right">
|
||
<?php foreach (($newByYear ?? []) as $row): ?>
|
||
<tr><td class="text-center"><?= esc($row->yr) ?>년</td><td><?= number_format((int) $row->cnt) ?></td></tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($newByYear)): ?>
|
||
<tr><td colspan="2" class="text-center text-gray-400 py-2">없음</td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xs font-bold text-gray-700 mb-1">연도별 취소/비활성 (등록일 기준)</h3>
|
||
<div class="border border-gray-300 overflow-auto max-h-48">
|
||
<table class="w-full data-table text-xs">
|
||
<thead><tr><th>연도</th><th>건수</th></tr></thead>
|
||
<tbody class="text-right">
|
||
<?php foreach (($cancelByYear ?? []) as $row): ?>
|
||
<tr><td class="text-center"><?= esc($row->yr) ?>년</td><td><?= number_format((int) $row->cnt) ?></td></tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($cancelByYear)): ?>
|
||
<tr><td colspan="2" class="text-center text-gray-400 py-2">없음</td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</details>
|