사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.

통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
taekyoungc
2026-06-01 16:15:15 +09:00
parent 21e7b91871
commit 0f1d414f37
129 changed files with 18068 additions and 1585 deletions

View File

@@ -1,97 +1,258 @@
<?= view('components/print_header', ['printTitle' => '판매 대장']) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<?php
declare(strict_types=1);
/** @var list<object> $shops */
/** @var list<object> $agencies */
/** @var list<array<string,mixed>> $ledgerRows */
/** @var int $saleLineCount */
/** @var string $startDate */
/** @var string $endDate */
/** @var string $mode */
/** @var int $dsIdx */
/** @var int $saIdx */
/** @var list<string> $cats */
/** @var string $lgName */
/** @var string $filterAgencyLabel */
/** @var list<string> $printSubtitleLines */
$printTitle = ($mode ?? 'daily') === 'daily' ? '[지정판매소] 일자별 판매대장' : '[지정판매소] 기간별 판매대장';
$printDate = date('Y-m-d');
$printExtraLines = $printSubtitleLines ?? [];
$catKeys = ['general', 'food', 'sticker', 'reuse', 'apt', 'public_use', 'container', 'waste'];
$catLabels = [
'general' => '일반용',
'food' => '음식물',
'sticker' => '스티커',
'reuse' => '재사용',
'apt' => '공동주택용',
'public_use' => '공공용',
'container' => '용기',
'waste' => '폐기물',
];
$exportParams = [
'start_date' => $startDate,
'end_date' => $endDate,
'mode' => $mode,
'ds_idx' => $dsIdx,
'sa_idx' => $saIdx ?? 0,
'export' => '1',
];
if ($cats !== []) {
$exportParams['cat'] = $cats;
}
$excelUrl = mgmt_url('reports/sales-ledger?' . http_build_query($exportParams));
?>
<?= view('components/print_header', [
'printTitle' => $printTitle,
'printDate' => $printDate,
'printExtraLines' => $printExtraLines,
]) ?>
<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>
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<span class="text-sm font-bold text-gray-700">지정 판매소 판매 대장</span>
<div class="flex flex-wrap gap-2">
<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="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
</div>
</div>
</section>
<section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= mgmt_url('reports/sales-ledger') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">시작일</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">~</label>
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">조회방식</label>
<select name="mode" class="border border-gray-300 rounded px-2 py-1 text-sm">
<option value="daily" <?= ($mode ?? '') === 'daily' ? 'selected' : '' ?>>일자별</option>
<option value="period" <?= ($mode ?? '') === 'period' ? 'selected' : '' ?>>기간별</option>
</select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<section class="p-3 bg-white border-b border-gray-200 no-print">
<form method="get" action="<?= mgmt_url('reports/sales-ledger') ?>" id="sales-ledger-form" class="space-y-3 text-sm">
<div class="flex flex-wrap items-end gap-3">
<div>
<label class="block text-gray-600 mb-0.5">조회일자</label>
<div class="flex items-center gap-1">
<input type="date" name="start_date" value="<?= esc($startDate) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<span>~</span>
<input type="date" name="end_date" value="<?= esc($endDate) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
</div>
</div>
<div>
<label class="block text-gray-600 mb-0.5">지정판매소</label>
<select name="ds_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[14rem] max-w-[20rem]">
<option value="0">전체</option>
<?php foreach ($shops as $s): ?>
<?php $sid = (int) ($s->ds_idx ?? 0); ?>
<option value="<?= esc((string) $sid) ?>" <?= $dsIdx === $sid ? 'selected' : '' ?>>
<?= esc(trim((string) ($s->ds_shop_no ?? '') . ' ' . (string) ($s->ds_name ?? ''))) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-gray-600 mb-0.5">대행소</label>
<select name="sa_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[12rem] max-w-[20rem]">
<option value="0">전체</option>
<?php foreach ($agencies ?? [] as $agency): ?>
<?php $aid = (int) ($agency->sa_idx ?? 0); ?>
<option value="<?= esc((string) $aid) ?>" <?= (int) ($saIdx ?? 0) === $aid ? 'selected' : '' ?>>
<?= esc(trim((string) ($agency->sa_name ?? ''))) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<span class="block text-gray-600 mb-0.5">집계 방식</span>
<div class="flex gap-3">
<label class="inline-flex items-center gap-1"><input type="radio" name="mode" value="daily" <?= $mode === 'daily' ? 'checked' : '' ?>/> 일자별</label>
<label class="inline-flex items-center gap-1"><input type="radio" name="mode" value="period" <?= $mode === 'period' ? 'checked' : '' ?>/> 기간별</label>
</div>
</div>
</div>
<fieldset class="border border-gray-200 rounded p-2">
<legend class="text-xs text-gray-600 px-1">품목</legend>
<div class="flex flex-wrap gap-x-4 gap-y-1">
<label class="inline-flex items-center gap-1">
<input type="checkbox" name="cat[]" value="all" id="cat-all" <?= $cats === [] ? 'checked' : '' ?>/>
전체
</label>
<?php foreach ($catKeys as $ck): ?>
<label class="inline-flex items-center gap-1">
<input type="checkbox" name="cat[]" value="<?= esc($ck, 'attr') ?>" class="cat-item" <?= in_array($ck, $cats, true) ? 'checked' : '' ?>/>
<?= esc($catLabels[$ck] ?? $ck) ?>
</label>
<?php endforeach; ?>
</div>
</fieldset>
<div>
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
</div>
</form>
</section>
<?php if (($mode ?? 'daily') === 'daily'): ?>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th>판매일</th>
<th>판매소</th>
<th>봉투코드</th>
<th>봉투명</th>
<th>구분</th>
<th>수량</th>
<th>금액</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($result as $row): ?>
<tr>
<td class="text-center"><?= esc($row->bs_sale_date) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td class="text-center">
<?php
$typeMap = ['sale' => '판매', 'return' => '반품'];
echo esc($typeMap[$row->bs_type] ?? $row->bs_type);
?>
</td>
<td><?= number_format((int) $row->total_qty) ?></td>
<td><?= number_format((int) $row->total_amount) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($result)): ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<section class="p-3 bg-white sales-ledger-report-section">
<style>
@media print {
/* 일계표 등 다른 리포트와 동일: 브라우저 기본 세로 A4 (landscape 지정 안 함) */
.sales-ledger-screen-title { display: none !important; }
.sales-ledger-report-section { padding: 0 !important; }
.sales-ledger-scroll-wrap {
overflow: visible !important;
border: 1px solid #333 !important;
}
#sales-ledger-table {
font-size: 7.5pt !important;
width: 100% !important;
table-layout: fixed !important;
}
#sales-ledger-table th,
#sales-ledger-table td {
min-width: 0 !important;
padding: 3px 4px !important;
white-space: normal !important;
word-break: break-word;
overflow-wrap: anywhere;
line-height: 1.35;
vertical-align: middle;
}
#sales-ledger-table th {
font-size: 7pt !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
/* 세로 A4 폭에 맞춘 열 비율 (긴 칸은 줄바꿈) */
#sales-ledger-table .sl-col-date { width: 9%; }
#sales-ledger-table .sl-col-designation { width: 9%; }
#sales-ledger-table .sl-col-shop { width: 10%; }
#sales-ledger-table .sl-col-rep { width: 7%; }
#sales-ledger-table .sl-col-addr { width: 18%; }
#sales-ledger-table .sl-col-product { width: 12%; }
#sales-ledger-table .sl-col-num { width: 7%; }
#sales-ledger-table.sl-period .sl-col-addr { width: 22%; }
#sales-ledger-table.sl-period .sl-col-product { width: 14%; }
}
@media screen {
#sales-ledger-table th,
#sales-ledger-table td {
padding: 4px 8px;
line-height: 1.45;
font-size: 13px;
vertical-align: middle;
}
#sales-ledger-table .sl-col-date,
#sales-ledger-table .sl-col-num { white-space: nowrap; }
#sales-ledger-table .sl-col-addr,
#sales-ledger-table .sl-col-shop,
#sales-ledger-table .sl-col-product {
white-space: normal;
word-break: break-word;
}
}
</style>
<div class="mb-2 text-center sales-ledger-screen-title no-print">
<h1 class="text-lg font-bold m-0"><?= esc($printTitle) ?></h1>
<p class="text-sm text-gray-700 m-1"><?= esc(trim(($lgName ?? '') . ' / 지정판매소: ' . ($filterShopLabel ?? '') . ' / 대행소: ' . ($filterAgencyLabel ?? '전체'))) ?></p>
<p class="text-xs text-gray-500 m-0">(단위: 매 / 원) · <?= esc($startDate) ?> ~ <?= esc($endDate) ?></p>
</div>
<?php else: ?>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th>판매소</th>
<th>봉투코드</th>
<th>봉투명</th>
<th>판매수량</th>
<th>판매금액</th>
<th>반품수량</th>
<th>반품금액</th>
<th>계(수량)</th>
<th>계(금액)</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($result as $row): ?>
<tr>
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
<td><?= number_format((int) $row->sale_qty) ?></td>
<td><?= number_format((int) $row->sale_amount) ?></td>
<td><?= number_format((int) $row->return_qty) ?></td>
<td><?= number_format((int) $row->return_amount) ?></td>
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($result)): ?>
<tr><td colspan="9" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<div class="sales-ledger-scroll-wrap border border-gray-300 overflow-auto">
<table class="w-full data-table text-sm <?= ($mode ?? 'daily') === 'period' ? 'sl-period' : '' ?>" id="sales-ledger-table">
<thead>
<tr>
<?php if (($mode ?? 'daily') === 'daily'): ?>
<th class="sl-col-date">일자</th>
<?php endif; ?>
<th class="sl-col-designation">지정번호</th>
<th class="sl-col-shop text-left">판매소명</th>
<th class="sl-col-rep">대표자</th>
<th class="sl-col-addr text-left">소재지</th>
<th class="sl-col-product text-left">품명</th>
<th class="text-right sl-col-num">판매량</th>
<th class="text-right sl-col-num">판매금액</th>
<th class="text-right sl-col-num">수수료</th>
<th class="text-right sl-col-num">총액</th>
</tr>
</thead>
<tbody>
<?php foreach ($ledgerRows as $r): ?>
<?php
$kind = (string) ($r['kind'] ?? 'data');
$trClass = $kind === 'subtotal' ? 'bg-gray-50 font-semibold' : ($kind === 'grand' ? 'bg-amber-50 font-bold' : '');
?>
<tr class="<?= esc($trClass, 'attr') ?>">
<?php if (($mode ?? 'daily') === 'daily'): ?>
<td class="text-center sl-col-date"><?= esc((string) ($r['sale_date'] ?? '')) ?></td>
<?php endif; ?>
<td class="text-center sl-col-designation"><?= esc((string) ($r['designation_no'] ?? '')) ?></td>
<td class="text-left pl-1 sl-col-shop"><?= esc((string) ($r['shop_name'] ?? '')) ?></td>
<td class="text-center sl-col-rep"><?= esc((string) ($r['rep_name'] ?? '')) ?></td>
<td class="text-left pl-1 sl-col-addr"><?= esc((string) ($r['address'] ?? '')) ?></td>
<td class="text-left pl-1 sl-col-product"><?= esc((string) ($r['product_name'] ?? '')) ?></td>
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['qty'] ?? '')) ?></td>
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['amount'] ?? '')) ?></td>
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['fee'] ?? '')) ?></td>
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['total'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php if ($ledgerRows === []): ?>
<tr><td colspan="<?= ($mode ?? 'daily') === 'daily' ? '10' : '9' ?>" class="text-center text-gray-400 py-6">조회된 판매 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<p class="text-sm text-gray-700 mt-2 mb-0 no-print">판매건수(상세 행): <?= number_format((int) ($saleLineCount ?? 0)) ?>건</p>
</section>
<script>
(function () {
const form = document.getElementById('sales-ledger-form');
const catAll = document.getElementById('cat-all');
const items = () => Array.from(document.querySelectorAll('.cat-item'));
if (!form || !catAll) return;
catAll.addEventListener('change', () => {
if (catAll.checked) items().forEach((el) => { el.checked = false; });
});
items().forEach((el) => {
el.addEventListener('change', () => {
if (el.checked) catAll.checked = false;
if (!items().some((x) => x.checked)) catAll.checked = true;
});
});
form.addEventListener('submit', () => {
if (catAll.checked) items().forEach((el) => { el.checked = false; });
});
})();
</script>