Files
jongryangje/app/Views/admin/designated_shop/status.php
taekyoungc a8afaf4af2 style: 표/패널 UI 전면 통일 + 화면설명 드로어·글씨크기·탭 개선
표 디자인
- 모든 표를 가벼운 스타일로 통일(.data-table 경량화: 작은 회색 헤더·연한 구분선·hover)
- 표/패널 바깥 테두리 둥글게(rounded-lg) 일괄 적용, 표 래퍼에 패딩 카드(p-4) 통일
- 표 헤더·데이터 정렬을 전 화면 좌측 기준으로 통일
  - .data-table th/td text-align:left (전역), 흩어진 center/right 정렬 정리
  - 재디자인 Tailwind 표(포장단위·단가·기본코드·담당자·업체·판매대행소·무료대상자·지정판매소)도 셀 좌측화
- 기본정보관리 등 나머지 소메뉴 표를 기본 코드 관리 스타일(가벼운 표·상태 pill)로 재디자인

워크스페이스/공통
- "이 화면 설명" → 새 탭 대신 우측 드로어 팝업(현재 화면과 동시에 보기, Esc·드래그 폭조절)
- 상단바 글씨 크기 조절(A−/A+), 작업 내용에 zoom 적용
- 탭 최대치 도달 시 자동 삭제 대신 안내 토스트, "모두 닫기"(업무 현황 탭은 보존)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:26:36 +09:00

389 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
background: #fff;
max-width: 100%;
}
@media print {
.ds-status-x-scroll { overflow: visible !important; }
}
.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;
}
.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: #fff;
}
.ds-status-x-scroll td.sticky-num {
background: #fff;
text-align: center;
}
.ds-status-x-scroll tr.sum-row td.sticky-num {
background: #f9fafb;
}
.ds-status-x-scroll th.sticky-region,
.ds-status-x-scroll td.sticky-region {
position: sticky;
left: 3rem;
z-index: 2;
background: #fff;
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: #f9fafb;
}
.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 w-full text-[13px]">
<thead>
<tr class="text-left text-[11px] font-semibold text-gray-500 border-b border-gray-200">
<th class="sticky-num py-2.5 px-2 text-center w-12">순번</th>
<th class="sticky-region py-2.5 px-2"><?= esc($regionColLabel) ?></th>
<th class="py-2.5 px-2 text-left">
<span class="ds-help">구코드 <span class="ds-help-badge" tabindex="0" data-tip="지정판매소에 저장된 구·군 코드 값">?</span></span>
</th>
<th class="py-2.5 px-2 text-left">
<span class="ds-help">종전 <span class="ds-help-badge" tabindex="0" data-tip="전년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(전년도말)
</th>
<th class="py-2.5 px-2 text-left">
<span class="ds-help">지정 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 지정일이 속한 신규 지정 건수">?</span></span>(<?= $ry ?>년)
</th>
<th class="py-2.5 px-2 text-left">
<span class="ds-help">취소 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 폐업/해지 전환일이 속한 건수">?</span></span>(<?= $ry ?>년)
</th>
<th class="py-2.5 px-2 text-left">
<span class="ds-help">현행 <span class="ds-help-badge" tabindex="0" data-tip="조회년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(금년도말)
</th>
<th class="py-2.5 px-2 text-left">
<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="py-2.5 px-2 text-left">
<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="py-2.5 px-2 text-left">
<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="py-2.5 px-2 text-left">
<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 class="border-b border-gray-200 last:border-0 hover:bg-gray-50">
<td class="sticky-num py-2.5 px-2 text-center text-gray-500"><?= $rowNo ?></td>
<td class="sticky-region py-2.5 px-2 font-medium text-gray-900" title="<?= esc($row->region_label) ?>"><?= esc($row->region_label) ?></td>
<td class="py-2.5 px-2 text-left font-mono text-[12px] text-gray-700"><?= esc((string) ($row->gugun_code ?? '')) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format($prev) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format((int) $row->designated_y) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format((int) $row->cancelled_y) ?></td>
<td class="py-2.5 px-2 font-medium text-gray-900"><?= number_format($curr) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format((int) ($row->delta_curr_prev ?? 0)) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format((int) ($row->delta_des_cancel ?? 0)) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= $pctShare ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= $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 border-b border-gray-200 last:border-0">
<td class="sticky-num py-2.5 px-2 text-center text-gray-500">—</td>
<td class="sticky-region py-2.5 px-2 text-gray-900"><?= esc($districtTotal->region_label) ?></td>
<td class="py-2.5 px-2 text-left text-gray-500">—</td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) $districtTotal->prev_end) ?></td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) $districtTotal->designated_y) ?></td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) $districtTotal->cancelled_y) ?></td>
<td class="py-2.5 px-2 text-gray-900"><?= number_format((int) $districtTotal->curr_end) ?></td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) ($districtTotal->delta_curr_prev ?? 0)) ?></td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) ($districtTotal->delta_des_cancel ?? 0)) ?></td>
<td class="py-2.5 px-2 text-gray-700">100</td>
<td class="py-2.5 px-2 text-gray-700">
<?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="overflow-auto max-h-56">
<table class="w-full text-[13px]">
<thead>
<tr class="text-left text-[11px] font-semibold text-gray-500 border-b border-gray-200">
<th class="py-2.5 px-2 text-left">동</th>
<th class="py-2.5 px-2 text-left">종전</th>
<th class="py-2.5 px-2 text-left">지정</th>
<th class="py-2.5 px-2 text-left">취소</th>
<th class="py-2.5 px-2 text-left">현행</th>
<th class="py-2.5 px-2 text-left">증감</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($zoneRows as $z): ?>
<tr class="border-b border-gray-200 last:border-0 hover:bg-gray-50">
<td class="py-2.5 px-2 text-left font-medium text-gray-900"><?= esc((string) $z->zone_label) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format((int) $z->prev_end) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format((int) $z->designated_y) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= number_format((int) $z->cancelled_y) ?></td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) $z->curr_end) ?></td>
<td class="py-2.5 px-2 text-gray-600"><?= 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 rounded-lg 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="overflow-auto max-h-48">
<table class="w-full text-[13px]">
<thead>
<tr class="text-left text-[11px] font-semibold text-gray-500 border-b border-gray-200">
<th class="py-2.5 px-2 text-left">연도</th>
<th class="py-2.5 px-2 text-left">건수</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach (($newByYear ?? []) as $row): ?>
<tr class="border-b border-gray-200 last:border-0 hover:bg-gray-50">
<td class="py-2.5 px-2 text-left font-medium text-gray-900"><?= esc($row->yr) ?>년</td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) $row->cnt) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($newByYear)): ?>
<tr><td colspan="2" class="text-center text-gray-400 py-6">없음</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div>
<h3 class="text-xs font-bold text-gray-700 mb-1">연도별 취소/비활성 (등록일 기준)</h3>
<div class="overflow-auto max-h-48">
<table class="w-full text-[13px]">
<thead>
<tr class="text-left text-[11px] font-semibold text-gray-500 border-b border-gray-200">
<th class="py-2.5 px-2 text-left">연도</th>
<th class="py-2.5 px-2 text-left">건수</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach (($cancelByYear ?? []) as $row): ?>
<tr class="border-b border-gray-200 last:border-0 hover:bg-gray-50">
<td class="py-2.5 px-2 text-left font-medium text-gray-900"><?= esc($row->yr) ?>년</td>
<td class="py-2.5 px-2 text-gray-700"><?= number_format((int) $row->cnt) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($cancelByYear)): ?>
<tr><td colspan="2" class="text-center text-gray-400 py-6">없음</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</details>