2026-06-01 16:15:15 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Libraries;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 통계 분석 관리 (전년대비 / 월별·계절별 추이)
|
2026-06-08 00:46:51 +09:00
|
|
|
*
|
|
|
|
|
* 월별·계절별 추이·전년대비: bs_type = sale 판매량·판매금액만 집계 (반품·취소 제외)
|
2026-06-01 16:15:15 +09:00
|
|
|
*/
|
|
|
|
|
class BagAnalyticsReportBuilder
|
|
|
|
|
{
|
|
|
|
|
private \CodeIgniter\Database\BaseConnection $db;
|
|
|
|
|
|
|
|
|
|
/** @var array<string, string> */
|
|
|
|
|
private array $bagNames = [];
|
|
|
|
|
|
2026-06-08 00:46:51 +09:00
|
|
|
/** 판매(bs_type=sale) 낱장 수량만 합산 */
|
|
|
|
|
private function saleQtySql(): string
|
|
|
|
|
{
|
|
|
|
|
return "CASE WHEN bs.bs_type = 'sale' THEN ABS(bs.bs_qty) ELSE 0 END";
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 16:15:15 +09:00
|
|
|
/**
|
|
|
|
|
* @return array<string, array{label: string, months_label: string, months: list<int>, cross_year: bool}>
|
|
|
|
|
*/
|
|
|
|
|
public static function seasonCatalog(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'spring' => ['label' => '봄', 'months_label' => '3~5월', 'months' => [3, 4, 5], 'cross_year' => false],
|
|
|
|
|
'summer' => ['label' => '여름', 'months_label' => '6~8월', 'months' => [6, 7, 8], 'cross_year' => false],
|
|
|
|
|
'autumn' => ['label' => '가을', 'months_label' => '9~11월', 'months' => [9, 10, 11], 'cross_year' => false],
|
|
|
|
|
'winter' => ['label' => '겨울', 'months_label' => '전년12·1~2월', 'months' => [12, 1, 2], 'cross_year' => true],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function normalizeSeason(string $season): string
|
|
|
|
|
{
|
|
|
|
|
$raw = trim($season);
|
|
|
|
|
$aliases = [
|
|
|
|
|
'봄' => 'spring',
|
|
|
|
|
'여름' => 'summer',
|
|
|
|
|
'가을' => 'autumn',
|
|
|
|
|
'겨울' => 'winter',
|
|
|
|
|
];
|
|
|
|
|
$key = $aliases[$raw] ?? strtolower($raw);
|
|
|
|
|
$catalog = self::seasonCatalog();
|
|
|
|
|
|
|
|
|
|
return isset($catalog[$key]) ? $key : 'spring';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
|
|
|
|
|
{
|
|
|
|
|
$this->db = $db ?? \Config\Database::connect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array{
|
|
|
|
|
* gugunOptions: list<array{code: string, name: string}>,
|
|
|
|
|
* agencies: list<object>,
|
|
|
|
|
* lgName: string,
|
|
|
|
|
* gugunLabel: string
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
public function loadFilterOptions(int $lgIdx): array
|
|
|
|
|
{
|
|
|
|
|
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
|
|
|
|
|
$lgName = $lgRow ? trim((string) ($lgRow->lg_name ?? '')) : '';
|
|
|
|
|
|
|
|
|
|
$gugunRows = $this->db->query("
|
|
|
|
|
SELECT DISTINCT ds_gugun_code AS code
|
|
|
|
|
FROM designated_shop
|
|
|
|
|
WHERE ds_lg_idx = ? AND ds_gugun_code != ''
|
|
|
|
|
ORDER BY ds_gugun_code
|
|
|
|
|
", [$lgIdx])->getResultArray();
|
|
|
|
|
|
|
|
|
|
$gugunOptions = [['code' => '', 'name' => '전체']];
|
|
|
|
|
foreach ($gugunRows as $row) {
|
|
|
|
|
$code = trim((string) ($row['code'] ?? ''));
|
|
|
|
|
if ($code === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$gugunOptions[] = ['code' => $code, 'name' => $this->gugunLabel($lgIdx, $code)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$agencies = model(\App\Models\SalesAgencyModel::class)
|
|
|
|
|
->where('sa_lg_idx', $lgIdx)
|
|
|
|
|
->orderForDisplay()
|
|
|
|
|
->findAll();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'gugunOptions' => $gugunOptions,
|
|
|
|
|
'agencies' => $agencies,
|
|
|
|
|
'lgName' => $lgName,
|
|
|
|
|
'gugunLabel' => '',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array{
|
|
|
|
|
* rows: list<array<string, mixed>>,
|
|
|
|
|
* months: list<int>,
|
|
|
|
|
* prevYear: int,
|
|
|
|
|
* year: int
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
public function buildYearOverYear(
|
|
|
|
|
int $lgIdx,
|
|
|
|
|
int $year,
|
|
|
|
|
string $gugunCode,
|
|
|
|
|
int $dsIdx,
|
|
|
|
|
bool $queried
|
|
|
|
|
): array {
|
|
|
|
|
$prevYear = $year - 1;
|
|
|
|
|
$months = range(1, 12);
|
|
|
|
|
if (! $queried) {
|
|
|
|
|
return ['rows' => [], 'months' => $months, 'prevYear' => $prevYear, 'year' => $year];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->loadBagNames($lgIdx);
|
|
|
|
|
$agg = $this->aggregateMonthlyByBag($lgIdx, $prevYear, $year, $gugunCode, $dsIdx);
|
|
|
|
|
|
|
|
|
|
$rows = [];
|
|
|
|
|
$codesFromAgg = array_map(static fn ($c): string => (string) $c, array_keys($agg));
|
|
|
|
|
foreach ($this->bagCodesForReport($lgIdx, $codesFromAgg) as $code) {
|
|
|
|
|
$code = (string) $code;
|
|
|
|
|
$name = $this->resolveBagName($code);
|
|
|
|
|
$rows[] = $this->yoyBlock($code, (string) $name, '수량', $agg, $prevYear, $year, $months, false);
|
|
|
|
|
$rows[] = $this->yoyBlock($code, (string) $name, '금액', $agg, $prevYear, $year, $months, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['rows' => $rows, 'months' => $months, 'prevYear' => $prevYear, 'year' => $year];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array{rows: list<array<string, mixed>>, meta: array<string, int>}
|
|
|
|
|
*/
|
|
|
|
|
public function buildMonthlyTrend(
|
|
|
|
|
int $lgIdx,
|
|
|
|
|
string $baseYm,
|
|
|
|
|
string $trendBasis,
|
|
|
|
|
float $deviationMin,
|
|
|
|
|
string $gugunCode,
|
|
|
|
|
int $saIdx,
|
|
|
|
|
bool $queried
|
|
|
|
|
): array {
|
|
|
|
|
$empty = ['rows' => [], 'meta' => ['shopCount' => 0, 'monthSalesShops' => 0]];
|
|
|
|
|
if (! $queried || ! preg_match('/^(\d{4})-(\d{2})$/', $baseYm, $m)) {
|
|
|
|
|
return $empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$year = (int) $m[1];
|
|
|
|
|
$month = (int) $m[2];
|
|
|
|
|
$shops = $this->loadShops($lgIdx, $gugunCode, $saIdx);
|
|
|
|
|
if ($shops === []) {
|
|
|
|
|
return $empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$monthlyByShop = $this->monthlyNetByShop($lgIdx, $year, $month, $gugunCode, $saIdx);
|
|
|
|
|
$avgByShop = $this->averageNetByShop($lgIdx, $year - 1, $gugunCode, $trendBasis, $month, $saIdx);
|
|
|
|
|
$prevYearSameMonth = $trendBasis === 'year_avg'
|
|
|
|
|
? $this->monthlyNetByShop($lgIdx, $year - 1, $month, $gugunCode, $saIdx)
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
$rows = [];
|
|
|
|
|
$monthSalesShops = 0;
|
|
|
|
|
foreach ($shops as $shop) {
|
|
|
|
|
$sid = (int) ($shop['ds_idx'] ?? 0);
|
|
|
|
|
$monthly = (float) ($monthlyByShop[$sid] ?? 0.0);
|
|
|
|
|
$avg = (float) ($avgByShop[$sid] ?? 0.0);
|
|
|
|
|
if ($trendBasis === 'year_avg' && $avg <= 0) {
|
|
|
|
|
$avg = (float) ($prevYearSameMonth[$sid] ?? 0.0);
|
|
|
|
|
}
|
|
|
|
|
if ($monthly > 0) {
|
|
|
|
|
$monthSalesShops++;
|
|
|
|
|
}
|
|
|
|
|
$diff = $monthly - $avg;
|
|
|
|
|
$pct = $avg > 0 ? round(($diff / $avg) * 100, 2) : ($monthly > 0 ? 100.0 : 0.0);
|
|
|
|
|
// 편차 N% 이상 = |편차(%)| ≥ N (증가·감소 모두)
|
|
|
|
|
if ($deviationMin > 0 && abs($pct) < $deviationMin) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$rows[] = [
|
|
|
|
|
'agency_name' => (string) ($shop['agency_name'] ?? ''),
|
|
|
|
|
'shop_no' => (string) ($shop['ds_shop_no'] ?? ''),
|
|
|
|
|
'shop_name' => (string) ($shop['ds_name'] ?? ''),
|
|
|
|
|
'rep_name' => (string) ($shop['ds_rep_name'] ?? ''),
|
|
|
|
|
'prev_avg' => (int) round($avg),
|
|
|
|
|
'monthly_qty' => (int) round($monthly),
|
|
|
|
|
'avg_diff' => (int) round($diff),
|
|
|
|
|
'deviation_pct'=> $pct,
|
|
|
|
|
'designated_at'=> (string) ($shop['ds_designated_at'] ?? ''),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['shop_no'], (string) $b['shop_no']));
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'rows' => $rows,
|
|
|
|
|
'meta' => [
|
|
|
|
|
'shopCount' => count($shops),
|
|
|
|
|
'monthSalesShops' => $monthSalesShops,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return list<array<string, mixed>>
|
|
|
|
|
*/
|
|
|
|
|
public function buildSeasonalTrend(
|
|
|
|
|
int $lgIdx,
|
|
|
|
|
int $baseYear,
|
|
|
|
|
string $season,
|
|
|
|
|
float $deviationMin,
|
|
|
|
|
string $gugunCode,
|
|
|
|
|
bool $queried
|
|
|
|
|
): array {
|
|
|
|
|
if (! $queried) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$seasonKey = self::normalizeSeason($season);
|
|
|
|
|
$seasonDef = self::seasonCatalog()[$seasonKey];
|
|
|
|
|
$months = $seasonDef['months'];
|
|
|
|
|
|
|
|
|
|
$saIdx = 0;
|
|
|
|
|
$shops = $this->loadShops($lgIdx, $gugunCode, $saIdx);
|
|
|
|
|
if ($shops === []) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$crossYear = (bool) ($seasonDef['cross_year'] ?? false);
|
|
|
|
|
$currentByShop = $this->seasonalNetByShop($lgIdx, $baseYear, $months, $gugunCode, $saIdx, $crossYear);
|
|
|
|
|
$prevByShop = $this->seasonalNetByShop($lgIdx, $baseYear - 1, $months, $gugunCode, $saIdx, $crossYear);
|
|
|
|
|
|
|
|
|
|
$rows = [];
|
|
|
|
|
foreach ($shops as $shop) {
|
|
|
|
|
$sid = (int) ($shop['ds_idx'] ?? 0);
|
|
|
|
|
$curr = (float) ($currentByShop[$sid] ?? 0.0);
|
|
|
|
|
$prev = (float) ($prevByShop[$sid] ?? 0.0);
|
|
|
|
|
$diff = $curr - $prev;
|
|
|
|
|
$pct = $prev > 0 ? round(($diff / $prev) * 100, 2) : ($curr > 0 ? 100.0 : 0.0);
|
|
|
|
|
if ($deviationMin > 0 && abs($pct) < $deviationMin) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$rows[] = [
|
|
|
|
|
'agency_name' => (string) ($shop['agency_name'] ?? ''),
|
|
|
|
|
'shop_name' => (string) ($shop['ds_name'] ?? ''),
|
|
|
|
|
'shop_no' => (string) ($shop['ds_shop_no'] ?? ''),
|
|
|
|
|
'rep_name' => (string) ($shop['ds_rep_name'] ?? ''),
|
|
|
|
|
'prev_season_avg'=> (int) round($prev),
|
|
|
|
|
'base_season_avg'=> (int) round($curr),
|
|
|
|
|
'avg_diff' => (int) round($diff),
|
|
|
|
|
'deviation_pct' => $pct,
|
|
|
|
|
'designated_at' => (string) ($shop['ds_designated_at'] ?? ''),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['shop_no'], (string) $b['shop_no']));
|
|
|
|
|
|
|
|
|
|
return $rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function gugunLabel(int $lgIdx, string $code): string
|
|
|
|
|
{
|
|
|
|
|
static $cache = [];
|
|
|
|
|
$key = $lgIdx . ':' . $code;
|
|
|
|
|
if (isset($cache[$key])) {
|
|
|
|
|
return $cache[$key];
|
|
|
|
|
}
|
|
|
|
|
$row = $this->db->table('code_detail cd')
|
|
|
|
|
->select('cd.cd_name')
|
|
|
|
|
->join('code_kind ck', 'ck.ck_idx = cd.cd_ck_idx', 'inner')
|
|
|
|
|
->where('ck.ck_code', 'G')
|
|
|
|
|
->where('cd.cd_lg_idx', $lgIdx)
|
|
|
|
|
->where('cd.cd_code', $code)
|
|
|
|
|
->get()
|
|
|
|
|
->getRowArray();
|
|
|
|
|
$cache[$key] = trim((string) ($row['cd_name'] ?? $code));
|
|
|
|
|
|
|
|
|
|
return $cache[$key];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function resolveBagName(string $code): string
|
|
|
|
|
{
|
|
|
|
|
if (isset($this->bagNames[$code])) {
|
|
|
|
|
return (string) $this->bagNames[$code];
|
|
|
|
|
}
|
|
|
|
|
if (ctype_digit($code) && isset($this->bagNames[(int) $code])) {
|
|
|
|
|
return (string) $this->bagNames[(int) $code];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $code;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function loadBagNames(int $lgIdx): void
|
|
|
|
|
{
|
|
|
|
|
if ($this->bagNames !== []) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
|
|
|
if (! $kindO) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) as $d) {
|
|
|
|
|
$code = trim((string) ($d->cd_code ?? ''));
|
|
|
|
|
if ($code !== '') {
|
|
|
|
|
$this->bagNames[$code] = trim((string) ($d->cd_name ?? $code));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param list<string> $codesFromAgg
|
|
|
|
|
* @return list<string>
|
|
|
|
|
*/
|
|
|
|
|
private function bagCodesForReport(int $lgIdx, array $codesFromAgg): array
|
|
|
|
|
{
|
|
|
|
|
$this->loadBagNames($lgIdx);
|
|
|
|
|
$codes = array_keys($this->bagNames);
|
|
|
|
|
if ($codesFromAgg !== []) {
|
|
|
|
|
$merged = array_merge($codes, $codesFromAgg);
|
|
|
|
|
$codes = [];
|
|
|
|
|
foreach ($merged as $c) {
|
|
|
|
|
$codes[] = (string) $c;
|
|
|
|
|
}
|
|
|
|
|
$codes = array_values(array_unique($codes));
|
|
|
|
|
sort($codes, SORT_STRING);
|
|
|
|
|
} else {
|
|
|
|
|
$codes = array_map(static fn ($c): string => (string) $c, $codes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $codes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<string, array<int, array<int, array{qty: float, amt: float}>>>
|
|
|
|
|
*/
|
|
|
|
|
private function aggregateMonthlyByBag(
|
|
|
|
|
int $lgIdx,
|
|
|
|
|
int $fromYear,
|
|
|
|
|
int $toYear,
|
|
|
|
|
string $gugunCode,
|
|
|
|
|
int $dsIdx
|
|
|
|
|
): array {
|
2026-06-08 00:46:51 +09:00
|
|
|
$saleQty = $this->saleQtySql();
|
2026-06-01 16:15:15 +09:00
|
|
|
$sql = "
|
|
|
|
|
SELECT bs.bs_bag_code AS bag_code, YEAR(bs.bs_sale_date) AS y, MONTH(bs.bs_sale_date) AS m,
|
2026-06-08 00:46:51 +09:00
|
|
|
SUM({$saleQty}) AS sale_qty,
|
|
|
|
|
SUM(CASE WHEN bs.bs_type = 'sale' THEN bs.bs_amount ELSE 0 END) AS sale_amt
|
2026-06-01 16:15:15 +09:00
|
|
|
FROM bag_sale bs
|
|
|
|
|
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
|
|
|
WHERE bs.bs_lg_idx = ?
|
|
|
|
|
AND YEAR(bs.bs_sale_date) BETWEEN ? AND ?
|
|
|
|
|
";
|
|
|
|
|
$params = [$lgIdx, $fromYear, $toYear];
|
|
|
|
|
if ($gugunCode !== '') {
|
|
|
|
|
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
|
|
|
$params[] = $gugunCode;
|
|
|
|
|
}
|
|
|
|
|
if ($dsIdx > 0) {
|
|
|
|
|
$sql .= ' AND bs.bs_ds_idx = ?';
|
|
|
|
|
$params[] = $dsIdx;
|
|
|
|
|
}
|
|
|
|
|
$sql .= ' GROUP BY bs.bs_bag_code, YEAR(bs.bs_sale_date), MONTH(bs.bs_sale_date)';
|
|
|
|
|
|
|
|
|
|
$agg = [];
|
|
|
|
|
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
|
|
|
|
$code = (string) ($row['bag_code'] ?? '');
|
|
|
|
|
$y = (int) ($row['y'] ?? 0);
|
|
|
|
|
$m = (int) ($row['m'] ?? 0);
|
|
|
|
|
if ($code === '' || $y <= 0 || $m <= 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$agg[$code][$y][$m] = [
|
2026-06-08 00:46:51 +09:00
|
|
|
'qty' => (float) ($row['sale_qty'] ?? 0),
|
|
|
|
|
'amt' => (float) ($row['sale_amt'] ?? 0),
|
2026-06-01 16:15:15 +09:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $agg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, array<int, array<int, array{qty: float, amt: float}>>> $agg
|
|
|
|
|
* @param list<int> $months
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
private function yoyBlock(
|
|
|
|
|
string $code,
|
|
|
|
|
string $name,
|
|
|
|
|
string $section,
|
|
|
|
|
array $agg,
|
|
|
|
|
int $prevYear,
|
|
|
|
|
int $year,
|
|
|
|
|
array $months,
|
|
|
|
|
bool $useAmount
|
|
|
|
|
): array {
|
|
|
|
|
$key = $useAmount ? 'amt' : 'qty';
|
|
|
|
|
$lines = [];
|
|
|
|
|
foreach ([$prevYear => (string) $prevYear . '년', $year => (string) $year . '년', 0 => '증감'] as $y => $label) {
|
|
|
|
|
$monthVals = [];
|
|
|
|
|
$total = 0.0;
|
|
|
|
|
foreach ($months as $mo) {
|
|
|
|
|
$v = 0.0;
|
|
|
|
|
if ($y === 0) {
|
|
|
|
|
$p = (float) ($agg[$code][$prevYear][$mo][$key] ?? 0);
|
|
|
|
|
$c = (float) ($agg[$code][$year][$mo][$key] ?? 0);
|
|
|
|
|
$v = $c - $p;
|
|
|
|
|
} else {
|
|
|
|
|
$v = (float) ($agg[$code][$y][$mo][$key] ?? 0);
|
|
|
|
|
}
|
|
|
|
|
$monthVals[$mo] = (int) round($v);
|
|
|
|
|
$total += $v;
|
|
|
|
|
}
|
|
|
|
|
$lines[] = ['label' => $label, 'months' => $monthVals, 'total' => (int) round($total)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'bag_code' => $code,
|
|
|
|
|
'bag_name' => $name,
|
|
|
|
|
'section' => $section,
|
|
|
|
|
'lines' => $lines,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return list<array<string, mixed>>
|
|
|
|
|
*/
|
|
|
|
|
/**
|
|
|
|
|
* @return array<string, string> 구·군코드 → 대행소명
|
|
|
|
|
*/
|
|
|
|
|
private function agencyNameByGugun(int $lgIdx): array
|
|
|
|
|
{
|
|
|
|
|
$best = [];
|
|
|
|
|
foreach ($this->db->query("
|
|
|
|
|
SELECT TRIM(bo.bo_gugun_code) AS code, bo.bo_agency_idx AS sa_idx, COUNT(*) AS cnt
|
|
|
|
|
FROM bag_order bo
|
|
|
|
|
WHERE bo.bo_lg_idx = ?
|
|
|
|
|
AND bo.bo_status = 'normal'
|
|
|
|
|
AND bo.bo_agency_idx IS NOT NULL
|
|
|
|
|
AND TRIM(bo.bo_gugun_code) != ''
|
|
|
|
|
GROUP BY TRIM(bo.bo_gugun_code), bo.bo_agency_idx
|
|
|
|
|
", [$lgIdx])->getResultArray() as $row) {
|
|
|
|
|
$code = (string) ($row['code'] ?? '');
|
|
|
|
|
$cnt = (int) ($row['cnt'] ?? 0);
|
|
|
|
|
if ($code === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (! isset($best[$code]) || $cnt > $best[$code]['cnt']) {
|
|
|
|
|
$best[$code] = ['cnt' => $cnt, 'sa_idx' => (int) ($row['sa_idx'] ?? 0)];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$names = [];
|
|
|
|
|
foreach ($best as $code => $info) {
|
|
|
|
|
$sa = model(\App\Models\SalesAgencyModel::class)->find($info['sa_idx']);
|
|
|
|
|
$names[$code] = $sa ? trim((string) ($sa->sa_name ?? '')) : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $names;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function loadShops(int $lgIdx, string $gugunCode, int $saIdx = 0): array
|
|
|
|
|
{
|
|
|
|
|
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
|
|
|
|
$agencyByGugun = $this->agencyNameByGugun($lgIdx);
|
|
|
|
|
|
|
|
|
|
$sql = '
|
|
|
|
|
SELECT ds.ds_idx, ds.ds_shop_no, ds.ds_name, ds.ds_rep_name, ds.ds_designated_at,
|
|
|
|
|
ds.ds_gugun_code';
|
|
|
|
|
if ($hasDsSa) {
|
|
|
|
|
$sql .= ', ds.ds_sa_idx';
|
|
|
|
|
}
|
|
|
|
|
$sql .= '
|
|
|
|
|
FROM designated_shop ds
|
|
|
|
|
WHERE ds.ds_lg_idx = ? AND ds.ds_state = 1
|
|
|
|
|
';
|
|
|
|
|
$params = [$lgIdx];
|
|
|
|
|
|
|
|
|
|
if ($gugunCode !== '') {
|
|
|
|
|
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
|
|
|
$params[] = $gugunCode;
|
|
|
|
|
}
|
|
|
|
|
if ($saIdx > 0 && $hasDsSa) {
|
|
|
|
|
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
|
|
|
$params[] = $saIdx;
|
|
|
|
|
}
|
|
|
|
|
$sql .= ' ORDER BY ds.ds_shop_no ASC, ds.ds_idx ASC';
|
|
|
|
|
|
|
|
|
|
$rows = $this->db->query($sql, $params)->getResultArray();
|
|
|
|
|
$saNames = [];
|
|
|
|
|
if ($hasDsSa) {
|
|
|
|
|
foreach (model(\App\Models\SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->findAll() as $sa) {
|
|
|
|
|
$saNames[(int) ($sa->sa_idx ?? 0)] = trim((string) ($sa->sa_name ?? ''));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($rows as &$row) {
|
|
|
|
|
$name = '';
|
|
|
|
|
if ($hasDsSa) {
|
|
|
|
|
$saidx = (int) ($row['ds_sa_idx'] ?? 0);
|
|
|
|
|
$name = $saNames[$saidx] ?? '';
|
|
|
|
|
}
|
|
|
|
|
if ($name === '') {
|
|
|
|
|
$code = trim((string) ($row['ds_gugun_code'] ?? ''));
|
|
|
|
|
$name = $agencyByGugun[$code] ?? '';
|
|
|
|
|
}
|
|
|
|
|
$row['agency_name'] = $name;
|
|
|
|
|
}
|
|
|
|
|
unset($row);
|
|
|
|
|
|
|
|
|
|
return $rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<int, float>
|
|
|
|
|
*/
|
|
|
|
|
private function monthlyNetByShop(int $lgIdx, int $year, int $month, string $gugunCode, int $saIdx = 0): array
|
|
|
|
|
{
|
|
|
|
|
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
2026-06-08 00:46:51 +09:00
|
|
|
$saleQty = $this->saleQtySql();
|
2026-06-01 16:15:15 +09:00
|
|
|
$sql = "
|
|
|
|
|
SELECT bs.bs_ds_idx AS ds_idx,
|
2026-06-08 00:46:51 +09:00
|
|
|
SUM({$saleQty}) AS sale_qty
|
2026-06-01 16:15:15 +09:00
|
|
|
FROM bag_sale bs
|
|
|
|
|
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
|
|
|
WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) = ?
|
|
|
|
|
AND bs.bs_ds_idx IS NOT NULL
|
|
|
|
|
";
|
|
|
|
|
$params = [$lgIdx, $year, $month];
|
|
|
|
|
if ($gugunCode !== '') {
|
|
|
|
|
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
|
|
|
$params[] = $gugunCode;
|
|
|
|
|
}
|
|
|
|
|
if ($saIdx > 0 && $hasDsSa) {
|
|
|
|
|
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
|
|
|
$params[] = $saIdx;
|
|
|
|
|
}
|
|
|
|
|
$sql .= ' GROUP BY bs.bs_ds_idx';
|
|
|
|
|
|
|
|
|
|
$map = [];
|
|
|
|
|
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
2026-06-08 00:46:51 +09:00
|
|
|
$map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['sale_qty'] ?? 0);
|
2026-06-01 16:15:15 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $map;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<int, float>
|
|
|
|
|
*/
|
|
|
|
|
private function averageNetByShop(
|
|
|
|
|
int $lgIdx,
|
|
|
|
|
int $year,
|
|
|
|
|
string $gugunCode,
|
|
|
|
|
string $trendBasis,
|
|
|
|
|
int $refMonth,
|
|
|
|
|
int $saIdx = 0
|
|
|
|
|
): array {
|
|
|
|
|
if ($trendBasis === 'month') {
|
|
|
|
|
return $this->monthlyNetByShop($lgIdx, $year, $refMonth, $gugunCode, $saIdx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
2026-06-08 00:46:51 +09:00
|
|
|
$saleQty = $this->saleQtySql();
|
2026-06-01 16:15:15 +09:00
|
|
|
$sql = "
|
|
|
|
|
SELECT bs.bs_ds_idx AS ds_idx,
|
2026-06-08 00:46:51 +09:00
|
|
|
SUM({$saleQty}) / 12 AS avg_qty
|
2026-06-01 16:15:15 +09:00
|
|
|
FROM bag_sale bs
|
|
|
|
|
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
|
|
|
WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ?
|
|
|
|
|
AND bs.bs_ds_idx IS NOT NULL
|
|
|
|
|
";
|
|
|
|
|
$params = [$lgIdx, $year];
|
|
|
|
|
if ($gugunCode !== '') {
|
|
|
|
|
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
|
|
|
$params[] = $gugunCode;
|
|
|
|
|
}
|
|
|
|
|
if ($saIdx > 0 && $hasDsSa) {
|
|
|
|
|
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
|
|
|
$params[] = $saIdx;
|
|
|
|
|
}
|
|
|
|
|
$sql .= ' GROUP BY bs.bs_ds_idx';
|
|
|
|
|
|
|
|
|
|
$map = [];
|
|
|
|
|
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
|
|
|
|
$map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['avg_qty'] ?? 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $map;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param list<int> $months
|
|
|
|
|
* @return array<int, float>
|
|
|
|
|
*/
|
|
|
|
|
private function seasonalNetByShop(
|
|
|
|
|
int $lgIdx,
|
|
|
|
|
int $year,
|
|
|
|
|
array $months,
|
|
|
|
|
string $gugunCode,
|
|
|
|
|
int $saIdx = 0,
|
|
|
|
|
bool $crossYearWinter = false
|
|
|
|
|
): array {
|
|
|
|
|
if ($months === []) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$divisor = count($months);
|
2026-06-08 00:46:51 +09:00
|
|
|
$saleQty = $this->saleQtySql();
|
2026-06-01 16:15:15 +09:00
|
|
|
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
|
|
|
|
|
|
|
|
|
if ($crossYearWinter) {
|
|
|
|
|
$sql = "
|
|
|
|
|
SELECT bs.bs_ds_idx AS ds_idx,
|
2026-06-08 00:46:51 +09:00
|
|
|
SUM({$saleQty}) / ? AS avg_qty
|
2026-06-01 16:15:15 +09:00
|
|
|
FROM bag_sale bs
|
|
|
|
|
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
|
|
|
WHERE bs.bs_lg_idx = ?
|
|
|
|
|
AND bs.bs_ds_idx IS NOT NULL
|
|
|
|
|
AND (
|
|
|
|
|
(YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) = 12)
|
|
|
|
|
OR (YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) IN (1, 2))
|
|
|
|
|
)
|
|
|
|
|
";
|
|
|
|
|
$params = [$divisor, $lgIdx, $year - 1, $year];
|
|
|
|
|
} else {
|
|
|
|
|
$placeholders = implode(',', array_fill(0, count($months), '?'));
|
|
|
|
|
$sql = "
|
|
|
|
|
SELECT bs.bs_ds_idx AS ds_idx,
|
2026-06-08 00:46:51 +09:00
|
|
|
SUM({$saleQty}) / ? AS avg_qty
|
2026-06-01 16:15:15 +09:00
|
|
|
FROM bag_sale bs
|
|
|
|
|
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
|
|
|
WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ?
|
|
|
|
|
AND MONTH(bs.bs_sale_date) IN ({$placeholders})
|
|
|
|
|
AND bs.bs_ds_idx IS NOT NULL
|
|
|
|
|
";
|
|
|
|
|
$params = array_merge([$divisor], [$lgIdx, $year], $months);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($gugunCode !== '') {
|
|
|
|
|
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
|
|
|
$params[] = $gugunCode;
|
|
|
|
|
}
|
|
|
|
|
if ($saIdx > 0 && $hasDsSa) {
|
|
|
|
|
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
|
|
|
$params[] = $saIdx;
|
|
|
|
|
}
|
|
|
|
|
$sql .= ' GROUP BY bs.bs_ds_idx';
|
|
|
|
|
|
|
|
|
|
$map = [];
|
|
|
|
|
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
|
|
|
|
$map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['avg_qty'] ?? 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $map;
|
|
|
|
|
}
|
|
|
|
|
}
|