Compare commits
15 Commits
clean/push
...
5c89c963ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c89c963ee | ||
|
|
05c479397b | ||
|
|
647d5f919d | ||
|
|
0b4c622b99 | ||
|
|
40db578e85 | ||
|
|
5d733ac0d8 | ||
|
|
2629644f90 | ||
|
|
c8d1612f0e | ||
|
|
48e5578611 | ||
|
|
078fa5d0c2 | ||
|
|
734a55833b | ||
|
|
72578f200c | ||
|
|
8e859f420d | ||
|
|
cd2d41b3d7 | ||
|
|
f22b1480a3 |
10
.cursor/rules/dependency-security.mdc
Normal file
10
.cursor/rules/dependency-security.mdc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description: 패키지 설치 전 승인·안정성 확인 및 공급망 보안 습관
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# 의존성·패키지 보안
|
||||||
|
|
||||||
|
- **새 패키지(npm, Composer 등)를 설치·추가하기 전에 반드시 사용자에게 먼저 물어본다.** 자동으로 `npm install`, `composer require` 등을 실행하지 않는다(사용자가 명시적으로 요청한 경우만).
|
||||||
|
- 새 버전을 제안할 때는 **공식 레지스트리(npmjs.org, packagist.org) 출처**인지 확인하고, **출시된 지 최소 며칠(가이드: 7일) 이상 지난 안정(stable) 버전**을 우선 제안한다. 방금 출시된 버전은 typosquat·피싱 패키지 위험이 있어 사용자에게 그 점을 짚어 준다.
|
||||||
|
- 락 파일(`package-lock.json`, `composer.lock`)을 대량 수정하거나 생소한 패키지를 넣지 않는다. 이상하면 사용자에게 중단하고 확인을 요청한다.
|
||||||
26
app/Config/Kakao.php
Normal file
26
app/Config/Kakao.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
use CodeIgniter\Config\BaseConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오 Developers — 내 애플리케이션 — 앱 키 — JavaScript 키
|
||||||
|
* .env 예: kakao.javascriptKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
*/
|
||||||
|
class Kakao extends BaseConfig
|
||||||
|
{
|
||||||
|
public string $javascriptKey = '';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$v = env('kakao.javascriptKey');
|
||||||
|
if (is_string($v) && $v !== '') {
|
||||||
|
$this->javascriptKey = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,13 @@ $routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void
|
|||||||
|
|
||||||
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
|
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
|
||||||
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
|
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
|
||||||
|
$routes->get('designated-shops/status/export', 'Admin\DesignatedShop::statusExport');
|
||||||
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
|
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
|
||||||
|
$routes->get('designated-shops/barcode', 'Admin\DesignatedShop::barcode');
|
||||||
|
$routes->post('designated-shops/barcode/print', 'Admin\DesignatedShop::barcodePrint');
|
||||||
|
$routes->get('designated-shops/district-new-cancel/export', 'Admin\DesignatedShop::districtNewCancelExport');
|
||||||
|
$routes->get('designated-shops/district-new-cancel', 'Admin\DesignatedShop::districtNewCancel');
|
||||||
|
$routes->get('designated-shops/browse', 'Admin\DesignatedShop::browse');
|
||||||
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
|
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
|
||||||
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
|
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
|
||||||
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
|
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ use App\Models\BagPriceModel;
|
|||||||
use App\Models\PackagingUnitModel;
|
use App\Models\PackagingUnitModel;
|
||||||
use App\Models\CompanyModel;
|
use App\Models\CompanyModel;
|
||||||
use App\Models\SalesAgencyModel;
|
use App\Models\SalesAgencyModel;
|
||||||
|
use App\Models\BagReceivingModel;
|
||||||
use App\Models\CodeKindModel;
|
use App\Models\CodeKindModel;
|
||||||
use App\Models\CodeDetailModel;
|
use App\Models\CodeDetailModel;
|
||||||
|
use App\Models\LocalGovernmentModel;
|
||||||
|
use App\Libraries\Blockchain\SqlLedger;
|
||||||
class BagOrder extends BaseController
|
class BagOrder extends BaseController
|
||||||
{
|
{
|
||||||
private BagOrderModel $orderModel;
|
private BagOrderModel $orderModel;
|
||||||
@@ -30,36 +33,76 @@ class BagOrder extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder = $this->orderModel->where('bo_lg_idx', $lgIdx);
|
$startMonth = (string) ($this->request->getGet('start_month') ?? date('Y-m'));
|
||||||
|
$endMonth = (string) ($this->request->getGet('end_month') ?? date('Y-m'));
|
||||||
// 기간 필터
|
if (! preg_match('/^\d{4}-\d{2}$/', $startMonth)) {
|
||||||
$startDate = $this->request->getGet('start_date');
|
$startMonth = date('Y-m');
|
||||||
$endDate = $this->request->getGet('end_date');
|
}
|
||||||
$status = $this->request->getGet('status');
|
if (! preg_match('/^\d{4}-\d{2}$/', $endMonth)) {
|
||||||
if ($startDate) $builder->where('bo_order_date >=', $startDate);
|
$endMonth = $startMonth;
|
||||||
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
}
|
||||||
if ($status) $builder->where('bo_status', $status);
|
if (strtotime($startMonth . '-01') > strtotime($endMonth . '-01')) {
|
||||||
|
[$startMonth, $endMonth] = [$endMonth, $startMonth];
|
||||||
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->paginate(20);
|
|
||||||
$pager = $this->orderModel->pager;
|
|
||||||
|
|
||||||
// 발주별 품목 합계
|
|
||||||
$itemSummary = [];
|
|
||||||
foreach ($list as $order) {
|
|
||||||
$items = $this->itemModel->where('boi_bo_idx', $order->bo_idx)->findAll();
|
|
||||||
$totalQty = 0; $totalAmt = 0;
|
|
||||||
foreach ($items as $it) { $totalQty += (int) $it->boi_qty_sheet; $totalAmt += (float) $it->boi_amount; }
|
|
||||||
$itemSummary[$order->bo_idx] = ['qty' => $totalQty, 'amount' => $totalAmt, 'count' => count($items)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 제작업체/대행소 이름 매핑
|
$companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
|
||||||
$companyMap = []; $agencyMap = [];
|
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
||||||
foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->findAll() as $c) $companyMap[$c->cp_idx] = $c->cp_name;
|
$receiveType = (string) ($this->request->getGet('receive_type') ?? 'all');
|
||||||
foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $a) {
|
if (! in_array($receiveType, ['all', 'received', 'pending'], true)) {
|
||||||
$agencyMap[$a->sa_idx] = '[' . ($a->sa_kind ?? '') . '] ' . ($a->sa_code ?? '') . ' — ' . ($a->sa_name ?? '');
|
$receiveType = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->renderWorkPage('발주 현황', 'admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status', 'pager'));
|
$companies = model(CompanyModel::class)
|
||||||
|
->where('cp_lg_idx', $lgIdx)
|
||||||
|
->where('cp_type', '제작업체')
|
||||||
|
->where('cp_state', 1)
|
||||||
|
->orderBy('cp_name', 'ASC')
|
||||||
|
->findAll();
|
||||||
|
$companyMap = [];
|
||||||
|
foreach ($companies as $company) {
|
||||||
|
$companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$agencyMap = [];
|
||||||
|
foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $agency) {
|
||||||
|
$agencyMap[(int) $agency->sa_idx] = (string) ($agency->sa_name ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
$bagNameMap = [];
|
||||||
|
foreach ($bagCodes as $code) {
|
||||||
|
$bagNameMap[(string) $code->cd_code] = (string) $code->cd_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportData = $this->buildOrderStatusRows(
|
||||||
|
$lgIdx,
|
||||||
|
$startMonth,
|
||||||
|
$endMonth,
|
||||||
|
$companyIdx,
|
||||||
|
$bagCode,
|
||||||
|
$receiveType,
|
||||||
|
$companyMap,
|
||||||
|
$agencyMap,
|
||||||
|
$bagNameMap
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->renderWorkPage(
|
||||||
|
'발주 현황',
|
||||||
|
'admin/bag_order/index',
|
||||||
|
[
|
||||||
|
'startMonth' => $startMonth,
|
||||||
|
'endMonth' => $endMonth,
|
||||||
|
'companyIdx' => $companyIdx,
|
||||||
|
'bagCode' => $bagCode,
|
||||||
|
'receiveType' => $receiveType,
|
||||||
|
'companyOptions' => $companies,
|
||||||
|
'bagCodeOptions' => $bagCodes,
|
||||||
|
'rows' => $reportData['rows'],
|
||||||
|
'groupRows' => $reportData['groupRows'],
|
||||||
|
'grandTotals' => $reportData['grandTotals'],
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function export()
|
public function export()
|
||||||
@@ -70,44 +113,240 @@ class BagOrder extends BaseController
|
|||||||
return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder = $this->orderModel->where('bo_lg_idx', $lgIdx);
|
$startMonth = (string) ($this->request->getGet('start_month') ?? date('Y-m'));
|
||||||
$startDate = $this->request->getGet('start_date');
|
$endMonth = (string) ($this->request->getGet('end_month') ?? date('Y-m'));
|
||||||
$endDate = $this->request->getGet('end_date');
|
if (! preg_match('/^\d{4}-\d{2}$/', $startMonth)) {
|
||||||
$status = $this->request->getGet('status');
|
$startMonth = date('Y-m');
|
||||||
if ($startDate) $builder->where('bo_order_date >=', $startDate);
|
}
|
||||||
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
if (! preg_match('/^\d{4}-\d{2}$/', $endMonth)) {
|
||||||
if ($status) $builder->where('bo_status', $status);
|
$endMonth = $startMonth;
|
||||||
|
}
|
||||||
|
if (strtotime($startMonth . '-01') > strtotime($endMonth . '-01')) {
|
||||||
|
[$startMonth, $endMonth] = [$endMonth, $startMonth];
|
||||||
|
}
|
||||||
|
|
||||||
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->findAll();
|
$companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
|
||||||
|
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
||||||
|
$receiveType = (string) ($this->request->getGet('receive_type') ?? 'all');
|
||||||
|
if (! in_array($receiveType, ['all', 'received', 'pending'], true)) {
|
||||||
|
$receiveType = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
$companyMap = [];
|
||||||
|
foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->findAll() as $company) {
|
||||||
|
$companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
|
||||||
|
}
|
||||||
|
$agencyMap = [];
|
||||||
|
foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $agency) {
|
||||||
|
$agencyMap[(int) $agency->sa_idx] = (string) ($agency->sa_name ?? '');
|
||||||
|
}
|
||||||
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
|
$bagNameMap = [];
|
||||||
|
foreach ($bagCodes as $code) {
|
||||||
|
$bagNameMap[(string) $code->cd_code] = (string) $code->cd_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportData = $this->buildOrderStatusRows(
|
||||||
|
$lgIdx,
|
||||||
|
$startMonth,
|
||||||
|
$endMonth,
|
||||||
|
$companyIdx,
|
||||||
|
$bagCode,
|
||||||
|
$receiveType,
|
||||||
|
$companyMap,
|
||||||
|
$agencyMap,
|
||||||
|
$bagNameMap
|
||||||
|
);
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
foreach ($reportData['rows'] as $row) {
|
||||||
foreach ($list as $row) {
|
if (! empty($row['is_subtotal'])) {
|
||||||
$items = $this->itemModel->where('boi_bo_idx', $row->bo_idx)->findAll();
|
$rows[] = [
|
||||||
$totalQty = 0;
|
'',
|
||||||
$totalAmt = 0;
|
'',
|
||||||
foreach ($items as $it) {
|
(string) ($row['label'] ?? '소계'),
|
||||||
$totalQty += (int) $it->boi_qty_sheet;
|
(int) ($row['order_qty'] ?? 0),
|
||||||
$totalAmt += (float) $it->boi_amount;
|
(int) ($row['received_qty'] ?? 0),
|
||||||
|
(int) ($row['pending_qty'] ?? 0),
|
||||||
|
(float) ($row['amount'] ?? 0),
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
$row->bo_idx,
|
(string) ($row['order_date'] ?? ''),
|
||||||
$row->bo_lot_no,
|
(string) ($row['company_name'] ?? ''),
|
||||||
$row->bo_order_date,
|
(string) ($row['bag_name'] ?? ''),
|
||||||
count($items),
|
(int) ($row['order_qty'] ?? 0),
|
||||||
$totalQty,
|
(int) ($row['received_qty'] ?? 0),
|
||||||
$totalAmt,
|
(int) ($row['pending_qty'] ?? 0),
|
||||||
$statusMap[$row->bo_status] ?? $row->bo_status,
|
(float) ($row['amount'] ?? 0),
|
||||||
|
(string) ($row['agency_name'] ?? ''),
|
||||||
|
'',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export_csv(
|
$gt = $reportData['grandTotals'] ?? [];
|
||||||
'발주현황_' . date('Ymd') . '.csv',
|
$rows[] = [
|
||||||
['번호', 'LOT번호', '발주일', '품목수', '총수량', '총금액', '상태'],
|
'',
|
||||||
|
'',
|
||||||
|
'총계',
|
||||||
|
(int) ($gt['order_qty'] ?? 0),
|
||||||
|
(int) ($gt['received_qty'] ?? 0),
|
||||||
|
(int) ($gt['pending_qty'] ?? 0),
|
||||||
|
(float) ($gt['amount'] ?? 0),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
export_xlsx(
|
||||||
|
'발주현황_' . date('Ymd'),
|
||||||
|
'발주현황',
|
||||||
|
['발주일자', '제작업체', '품명', '발주수량', '입고수량', '미입고수량', '발주금액', '입고처', '비고'],
|
||||||
$rows
|
$rows
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발주 현황(품목 기준) 행 및 소계를 만든다.
|
||||||
|
*/
|
||||||
|
private function buildOrderStatusRows(
|
||||||
|
int $lgIdx,
|
||||||
|
string $startMonth,
|
||||||
|
string $endMonth,
|
||||||
|
int $companyIdx,
|
||||||
|
string $bagCode,
|
||||||
|
string $receiveType,
|
||||||
|
array $companyMap,
|
||||||
|
array $agencyMap,
|
||||||
|
array $bagNameMap
|
||||||
|
): array {
|
||||||
|
$startDate = $startMonth . '-01';
|
||||||
|
$endDate = date('Y-m-t', strtotime($endMonth . '-01 00:00:00'));
|
||||||
|
|
||||||
|
$builder = $this->orderModel
|
||||||
|
->where('bo_lg_idx', $lgIdx)
|
||||||
|
->whereLatestHead($lgIdx)
|
||||||
|
->where('bo_order_date >=', $startDate)
|
||||||
|
->where('bo_order_date <=', $endDate)
|
||||||
|
->whereIn('bo_status', ['normal', 'cancelled'])
|
||||||
|
->orderBy('bo_order_date', 'DESC')
|
||||||
|
->orderBy('bo_idx', 'DESC');
|
||||||
|
if ($companyIdx > 0) {
|
||||||
|
$builder->where('bo_company_idx', $companyIdx);
|
||||||
|
}
|
||||||
|
$orders = $builder->findAll();
|
||||||
|
|
||||||
|
if (empty($orders)) {
|
||||||
|
return ['rows' => [], 'groupRows' => [], 'grandTotals' => ['order_qty' => 0, 'received_qty' => 0, 'pending_qty' => 0, 'amount' => 0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderIds = array_map(static fn($order) => (int) $order->bo_idx, $orders);
|
||||||
|
|
||||||
|
$itemsByOrder = [];
|
||||||
|
if (! empty($orderIds)) {
|
||||||
|
$allItems = $this->itemModel
|
||||||
|
->whereIn('boi_bo_idx', $orderIds)
|
||||||
|
->orderBy('boi_bo_idx', 'DESC')
|
||||||
|
->orderBy('boi_idx', 'ASC')
|
||||||
|
->findAll();
|
||||||
|
foreach ($allItems as $item) {
|
||||||
|
$boIdx = (int) ($item->boi_bo_idx ?? 0);
|
||||||
|
if (! isset($itemsByOrder[$boIdx])) {
|
||||||
|
$itemsByOrder[$boIdx] = [];
|
||||||
|
}
|
||||||
|
$itemsByOrder[$boIdx][] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$receivedMap = [];
|
||||||
|
$receivingRows = model(BagReceivingModel::class)
|
||||||
|
->select('br_bo_idx, br_bag_code, SUM(br_qty_sheet) as recv_qty')
|
||||||
|
->where('br_lg_idx', $lgIdx)
|
||||||
|
->whereIn('br_bo_idx', $orderIds)
|
||||||
|
->groupBy('br_bo_idx, br_bag_code')
|
||||||
|
->findAll();
|
||||||
|
foreach ($receivingRows as $received) {
|
||||||
|
$key = (int) ($received->br_bo_idx ?? 0) . '|' . (string) ($received->br_bag_code ?? '');
|
||||||
|
$receivedMap[$key] = (int) ($received->recv_qty ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$groupRows = [];
|
||||||
|
$grandTotals = ['order_qty' => 0, 'received_qty' => 0, 'pending_qty' => 0, 'amount' => 0.0];
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$boIdx = (int) ($order->bo_idx ?? 0);
|
||||||
|
$items = $itemsByOrder[$boIdx] ?? [];
|
||||||
|
$groupCount = 0;
|
||||||
|
$groupTotalOrder = 0;
|
||||||
|
$groupTotalReceived = 0;
|
||||||
|
$groupTotalPending = 0;
|
||||||
|
$groupTotalAmount = 0.0;
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$itemBagCode = (string) ($item->boi_bag_code ?? '');
|
||||||
|
if ($bagCode !== '' && $itemBagCode !== $bagCode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderQty = (int) ($item->boi_qty_sheet ?? 0);
|
||||||
|
$recvQty = (int) ($receivedMap[$boIdx . '|' . $itemBagCode] ?? 0);
|
||||||
|
if ($recvQty > $orderQty) {
|
||||||
|
$recvQty = $orderQty;
|
||||||
|
}
|
||||||
|
$pendingQty = max(0, $orderQty - $recvQty);
|
||||||
|
|
||||||
|
if ($receiveType === 'received' && $recvQty <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($receiveType === 'pending' && $pendingQty <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$amount = (float) ($item->boi_amount ?? 0);
|
||||||
|
$rows[] = [
|
||||||
|
'bo_idx' => $boIdx,
|
||||||
|
'order_date' => (string) ($order->bo_order_date ?? ''),
|
||||||
|
'company_name' => (string) ($companyMap[(int) ($order->bo_company_idx ?? 0)] ?? ''),
|
||||||
|
'bag_name' => (string) ($item->boi_bag_name ?? ($bagNameMap[$itemBagCode] ?? $itemBagCode)),
|
||||||
|
'order_qty' => $orderQty,
|
||||||
|
'received_qty' => $recvQty,
|
||||||
|
'pending_qty' => $pendingQty,
|
||||||
|
'amount' => $amount,
|
||||||
|
'agency_name' => (string) ($agencyMap[(int) ($order->bo_agency_idx ?? 0)] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
$groupCount++;
|
||||||
|
$groupTotalOrder += $orderQty;
|
||||||
|
$groupTotalReceived += $recvQty;
|
||||||
|
$groupTotalPending += $pendingQty;
|
||||||
|
$groupTotalAmount += $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($groupCount > 0) {
|
||||||
|
$groupRows[$boIdx] = $groupCount;
|
||||||
|
$rows[] = [
|
||||||
|
'bo_idx' => $boIdx,
|
||||||
|
'is_subtotal' => true,
|
||||||
|
'label' => '소계',
|
||||||
|
'order_qty' => $groupTotalOrder,
|
||||||
|
'received_qty' => $groupTotalReceived,
|
||||||
|
'pending_qty' => $groupTotalPending,
|
||||||
|
'amount' => $groupTotalAmount,
|
||||||
|
];
|
||||||
|
$grandTotals['order_qty'] += $groupTotalOrder;
|
||||||
|
$grandTotals['received_qty'] += $groupTotalReceived;
|
||||||
|
$grandTotals['pending_qty'] += $groupTotalPending;
|
||||||
|
$grandTotals['amount'] += $groupTotalAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['rows' => $rows, 'groupRows' => $groupRows, 'grandTotals' => $grandTotals];
|
||||||
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
@@ -119,18 +358,105 @@ class BagOrder extends BaseController
|
|||||||
// 봉투 종류 + 단가 + 포장단위
|
// 봉투 종류 + 단가 + 포장단위
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
$prices = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_state', 1)->findAll();
|
$priceMapRows = model(BagPriceModel::class)->latestActiveMapByBagCode($lgIdx);
|
||||||
$units = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll();
|
$units = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll();
|
||||||
$companies = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll();
|
|
||||||
$agencies = model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll();
|
|
||||||
|
|
||||||
return $this->renderWorkPage('발주 등록', 'admin/bag_order/create', compact('bagCodes', 'prices', 'units', 'companies', 'agencies'));
|
$companies = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll();
|
||||||
|
$associations = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '협회')->where('cp_state', 1)->findAll();
|
||||||
|
$agencies = model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll();
|
||||||
|
|
||||||
|
$companyMap = [];
|
||||||
|
foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->findAll() as $company) {
|
||||||
|
$companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
|
||||||
|
}
|
||||||
|
$agencyMap = [];
|
||||||
|
foreach ($agencies as $agency) {
|
||||||
|
$agencyMap[(int) $agency->sa_idx] = '[' . ($agency->sa_kind ?? '') . '] ' . ($agency->sa_code ?? '') . ' — ' . ($agency->sa_name ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$recentOrders = $this->orderModel
|
||||||
|
->where('bo_lg_idx', $lgIdx)
|
||||||
|
->whereLatestHead($lgIdx)
|
||||||
|
->orderBy('bo_order_date', 'DESC')
|
||||||
|
->orderBy('bo_idx', 'DESC')
|
||||||
|
->findAll(12);
|
||||||
|
|
||||||
|
$bagNameMap = [];
|
||||||
|
foreach ($bagCodes as $codeDetail) {
|
||||||
|
$bagNameMap[(string) $codeDetail->cd_code] = (string) $codeDetail->cd_name;
|
||||||
|
}
|
||||||
|
$priceMap = [];
|
||||||
|
foreach ($priceMapRows as $bagCode => $price) {
|
||||||
|
$priceMap[(string) $bagCode] = (float) ($price->bp_order_price ?? 0);
|
||||||
|
}
|
||||||
|
$unitMap = [];
|
||||||
|
foreach ($units as $unit) {
|
||||||
|
$unitMap[(string) $unit->pu_bag_code] = [
|
||||||
|
'boxPerPack' => (int) $unit->pu_box_per_pack,
|
||||||
|
'packPerSheet' => (int) $unit->pu_pack_per_sheet,
|
||||||
|
'totalPerBox' => (int) $unit->pu_total_per_box,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bagReferenceRows = [];
|
||||||
|
foreach ($bagCodes as $codeDetail) {
|
||||||
|
$bagCode = (string) $codeDetail->cd_code;
|
||||||
|
$unit = $unitMap[$bagCode] ?? ['boxPerPack' => 0, 'packPerSheet' => 0, 'totalPerBox' => 0];
|
||||||
|
$bagReferenceRows[] = [
|
||||||
|
'code' => $bagCode,
|
||||||
|
'name' => (string) ($bagNameMap[$bagCode] ?? ''),
|
||||||
|
'orderPrice' => (float) ($priceMap[$bagCode] ?? 0),
|
||||||
|
'boxPerPack' => (int) $unit['boxPerPack'],
|
||||||
|
'packPerSheet' => (int) $unit['packPerSheet'],
|
||||||
|
'totalPerBox' => (int) $unit['totalPerBox'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderWorkPage(
|
||||||
|
'발주 등록',
|
||||||
|
'admin/bag_order/create',
|
||||||
|
compact(
|
||||||
|
'bagCodes',
|
||||||
|
'units',
|
||||||
|
'companies',
|
||||||
|
'associations',
|
||||||
|
'agencies',
|
||||||
|
'recentOrders',
|
||||||
|
'companyMap',
|
||||||
|
'agencyMap',
|
||||||
|
'bagReferenceRows'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revise(int $id)
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
$order = $this->orderModel->find($id);
|
||||||
|
if (! $order || (int) $order->bo_lg_idx !== $lgIdx) {
|
||||||
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to(site_url('bag/order/revise/' . $id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
|
return redirect()->back()->withInput()->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceIdx = (int) ($this->request->getPost('bo_source_idx') ?? 0);
|
||||||
|
$sourceOrder = null;
|
||||||
|
if ($sourceIdx > 0) {
|
||||||
|
$sourceOrder = $this->orderModel->find($sourceIdx);
|
||||||
|
if (! $sourceOrder || (int) $sourceOrder->bo_lg_idx !== $lgIdx) {
|
||||||
|
return redirect()->back()->withInput()->with('error', '수정 대상 발주를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
'bo_order_date' => 'required|valid_date[Y-m-d]',
|
'bo_order_date' => 'required|valid_date[Y-m-d]',
|
||||||
@@ -141,65 +467,114 @@ class BagOrder extends BaseController
|
|||||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
||||||
|
$qtySheets = $this->request->getPost('item_qty_sheet') ?? [];
|
||||||
|
$qtyBoxes = $this->request->getPost('item_qty_box') ?? []; // 구 화면 호환
|
||||||
|
$postedUnitPrices = $this->request->getPost('item_unit_price');
|
||||||
|
$changeKind = (string) ($this->request->getPost('bo_change_mode') ?? 'meta');
|
||||||
|
if (! in_array($changeKind, ['price', 'meta', 'delete'], true)) {
|
||||||
|
$changeKind = 'meta';
|
||||||
|
}
|
||||||
|
$itemCount = count($bagCodes);
|
||||||
|
$normalizedItems = [];
|
||||||
|
for ($i = 0; $i < $itemCount; $i++) {
|
||||||
|
$code = trim((string) ($bagCodes[$i] ?? ''));
|
||||||
|
$qtySheet = (int) ($qtySheets[$i] ?? 0);
|
||||||
|
$qtyBox = (int) ($qtyBoxes[$i] ?? 0);
|
||||||
|
if ($code === '' || ($qtySheet <= 0 && $qtyBox <= 0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$normalizedItems[] = ['code' => $code, 'qtySheet' => $qtySheet, 'qtyBox' => $qtyBox];
|
||||||
|
}
|
||||||
|
if (empty($normalizedItems)) {
|
||||||
|
return redirect()->back()->withInput()->with('error', '최소 1개 이상의 봉투 수량을 입력해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$priceByCode = [];
|
||||||
|
if ($sourceOrder !== null && $changeKind === 'price' && is_array($postedUnitPrices)) {
|
||||||
|
for ($pi = 0; $pi < count($bagCodes); $pi++) {
|
||||||
|
$c = trim((string) ($bagCodes[$pi] ?? ''));
|
||||||
|
if ($c === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$raw = $postedUnitPrices[$pi] ?? null;
|
||||||
|
if ($raw !== null && $raw !== '' && is_numeric($raw)) {
|
||||||
|
$priceByCode[$c] = round((float) $raw, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$db->transStart();
|
$db->transStart();
|
||||||
|
|
||||||
// UUID 생성
|
try {
|
||||||
$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
if ($sourceOrder) {
|
||||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
$uuid = (string) $sourceOrder->bo_uuid;
|
||||||
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
$maxVerRow = $this->orderModel->selectMax('bo_version')->where('bo_uuid', $uuid)->first();
|
||||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
|
$latestVersion = ($maxVerRow !== null && isset($maxVerRow->bo_version)) ? (int) $maxVerRow->bo_version : 0;
|
||||||
|
$version = $latestVersion + 1;
|
||||||
// LOT 번호 생성
|
$lotNo = (string) $sourceOrder->bo_lot_no;
|
||||||
$lotNo = 'LOT-' . date('Ymd') . '-' . strtoupper(substr(md5($uuid), 0, 6));
|
} else {
|
||||||
|
$uuid = $this->generateUuidV4();
|
||||||
|
$version = 1;
|
||||||
|
$lotNo = $this->generateLotNo6();
|
||||||
|
}
|
||||||
|
|
||||||
$orderData = [
|
$orderData = [
|
||||||
'bo_uuid' => $uuid,
|
'bo_uuid' => $uuid,
|
||||||
'bo_version' => 1,
|
'bo_version' => $version,
|
||||||
'bo_lg_idx' => $lgIdx,
|
'bo_lg_idx' => $lgIdx,
|
||||||
'bo_gugun_code' => $this->request->getPost('bo_gugun_code') ?? '',
|
'bo_gugun_code' => $this->resolveGugunCodeFromLg($lgIdx),
|
||||||
'bo_dong_code' => $this->request->getPost('bo_dong_code') ?? '',
|
'bo_dong_code' => '',
|
||||||
'bo_company_idx' => $this->request->getPost('bo_company_idx') ?: null,
|
'bo_company_idx' => $this->request->getPost('bo_company_idx') ?: null,
|
||||||
'bo_agency_idx' => $this->request->getPost('bo_agency_idx') ?: null,
|
'bo_agency_idx' => $this->request->getPost('bo_agency_idx') ?: null,
|
||||||
'bo_fee_rate' => (float) ($this->request->getPost('bo_fee_rate') ?: 0),
|
'bo_fee_rate' => (float) ($this->request->getPost('bo_fee_rate') ?: 0),
|
||||||
'bo_order_date' => $this->request->getPost('bo_order_date'),
|
'bo_order_date' => $this->request->getPost('bo_order_date'),
|
||||||
|
'bo_bag_types' => '',
|
||||||
|
'bo_unit_prices' => '',
|
||||||
|
'bo_qty_boxes' => '',
|
||||||
'bo_lot_no' => $lotNo,
|
'bo_lot_no' => $lotNo,
|
||||||
'bo_status' => 'normal',
|
'bo_status' => 'normal',
|
||||||
'bo_orderer_idx' => session()->get('mb_idx'),
|
'bo_orderer_idx' => session()->get('mb_idx'),
|
||||||
'bo_regdate' => date('Y-m-d H:i:s'),
|
'bo_regdate' => date('Y-m-d H:i:s'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// SHA-256 해시
|
// 품목 입력 후 최종 payload 기준으로 해시를 계산하므로 우선 빈값으로 생성
|
||||||
$orderData['bo_hash'] = hash('sha256', json_encode($orderData));
|
$orderData['bo_hash'] = '';
|
||||||
|
|
||||||
$this->orderModel->insert($orderData);
|
$this->orderModel->insert($orderData);
|
||||||
$boIdx = (int) $this->orderModel->getInsertID();
|
$boIdx = (int) $this->orderModel->getInsertID();
|
||||||
|
|
||||||
// CT-05: 감사 로그
|
|
||||||
helper('audit');
|
|
||||||
audit_log('create', 'bag_order', $boIdx, null, array_merge($orderData, ['bo_idx' => $boIdx]));
|
|
||||||
|
|
||||||
// 품목 저장
|
// 품목 저장
|
||||||
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
$hashItems = [];
|
||||||
$qtyBoxes = $this->request->getPost('item_qty_box') ?? [];
|
$bagTypesForHeader = [];
|
||||||
foreach ($bagCodes as $i => $code) {
|
$unitPricesForHeader = [];
|
||||||
if (empty($code) || empty($qtyBoxes[$i])) continue;
|
$qtyBoxesForHeader = [];
|
||||||
$qtyBox = (int) $qtyBoxes[$i];
|
foreach ($normalizedItems as $item) {
|
||||||
|
$code = $item['code'];
|
||||||
|
$qtySheetInput = (int) ($item['qtySheet'] ?? 0);
|
||||||
|
$qtyBoxInput = (int) ($item['qtyBox'] ?? 0);
|
||||||
// 포장단위에서 낱장 환산
|
// 포장단위에서 낱장 환산
|
||||||
$unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first();
|
$unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first();
|
||||||
$totalPerBox = $unit ? (int) $unit->pu_total_per_box : 1;
|
$totalPerBox = $unit ? max(1, (int) $unit->pu_total_per_box) : 1;
|
||||||
$qtySheet = $qtyBox * $totalPerBox;
|
$qtySheet = $qtySheetInput > 0 ? $qtySheetInput : ($qtyBoxInput * $totalPerBox);
|
||||||
|
if ($qtySheet <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$qtyBox = intdiv($qtySheet, $totalPerBox);
|
||||||
|
|
||||||
// 단가
|
// 단가 (발주 변경·단가 구분 시 POST 단가 우선)
|
||||||
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $code)->where('bp_state', 1)->first();
|
$price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, $code);
|
||||||
$unitPrice = $price ? (float) $price->bp_order_price : 0;
|
$unitPrice = $price ? (float) $price->bp_order_price : 0;
|
||||||
|
if ($sourceOrder !== null && isset($priceByCode[$code])) {
|
||||||
|
$unitPrice = $priceByCode[$code];
|
||||||
|
}
|
||||||
|
|
||||||
// 봉투명
|
// 봉투명
|
||||||
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;
|
||||||
|
|
||||||
$this->itemModel->insert([
|
$itemData = [
|
||||||
'boi_bo_idx' => $boIdx,
|
'boi_bo_idx' => $boIdx,
|
||||||
'boi_bag_code' => $code,
|
'boi_bag_code' => $code,
|
||||||
'boi_bag_name' => $detail ? $detail->cd_name : '',
|
'boi_bag_name' => $detail ? $detail->cd_name : '',
|
||||||
@@ -207,14 +582,204 @@ class BagOrder extends BaseController
|
|||||||
'boi_qty_box' => $qtyBox,
|
'boi_qty_box' => $qtyBox,
|
||||||
'boi_qty_sheet' => $qtySheet,
|
'boi_qty_sheet' => $qtySheet,
|
||||||
'boi_amount' => $unitPrice * $qtySheet,
|
'boi_amount' => $unitPrice * $qtySheet,
|
||||||
]);
|
];
|
||||||
|
$this->itemModel->insert($itemData);
|
||||||
|
$hashItems[] = $itemData;
|
||||||
|
|
||||||
|
$bagTypesForHeader[] = [
|
||||||
|
'code' => $itemData['boi_bag_code'],
|
||||||
|
'name' => $itemData['boi_bag_name'],
|
||||||
|
];
|
||||||
|
$unitPricesForHeader[] = [
|
||||||
|
'code' => $itemData['boi_bag_code'],
|
||||||
|
'unit_price' => $itemData['boi_unit_price'],
|
||||||
|
];
|
||||||
|
$qtyBoxesForHeader[] = [
|
||||||
|
'code' => $itemData['boi_bag_code'],
|
||||||
|
'qty_box' => $itemData['boi_qty_box'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$db->transComplete();
|
$orderData['bo_bag_types'] = json_encode($bagTypesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
||||||
|
$orderData['bo_unit_prices'] = json_encode($unitPricesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
||||||
|
$orderData['bo_qty_boxes'] = json_encode($qtyBoxesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
||||||
|
|
||||||
|
// 최종 발주 데이터(헤더+품목) 해시
|
||||||
|
$hashPayload = $orderData;
|
||||||
|
$hashPayload['bo_idx'] = $boIdx;
|
||||||
|
$hashPayload['items'] = $hashItems;
|
||||||
|
$hashJson = json_encode($hashPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$orderHash = hash('sha256', $hashJson !== false ? $hashJson : (string) $boIdx);
|
||||||
|
$this->orderModel->update($boIdx, [
|
||||||
|
'bo_bag_types' => $orderData['bo_bag_types'],
|
||||||
|
'bo_unit_prices' => $orderData['bo_unit_prices'],
|
||||||
|
'bo_qty_boxes' => $orderData['bo_qty_boxes'],
|
||||||
|
'bo_hash' => $orderHash,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$beforeHash = $sourceOrder ? (string) ($sourceOrder->bo_hash ?? '') : '';
|
||||||
|
$seedFilePath = $this->generateBarcodeSeedFile($uuid, $version, $lotNo, $orderData, $hashItems, $orderHash);
|
||||||
|
$blockPayload = [
|
||||||
|
'bo_idx' => $boIdx,
|
||||||
|
'bo_uuid' => $uuid,
|
||||||
|
'bo_version' => $version,
|
||||||
|
'bo_lot_no' => $lotNo,
|
||||||
|
'bo_hash' => $orderHash,
|
||||||
|
'seed_file' => $seedFilePath,
|
||||||
|
'hash_chain' => $beforeHash !== '' ? [$beforeHash, $orderHash] : [$orderHash],
|
||||||
|
'order' => $orderData,
|
||||||
|
'items' => $hashItems,
|
||||||
|
];
|
||||||
|
$ledger = new SqlLedger();
|
||||||
|
$ledger->appendBlock(
|
||||||
|
$sourceOrder ? 'ORDER_UPDATE' : 'ORDER_CREATE',
|
||||||
|
$blockPayload,
|
||||||
|
$uuid,
|
||||||
|
$version,
|
||||||
|
session()->get('mb_idx') ? (int) session()->get('mb_idx') : null,
|
||||||
|
$lgIdx
|
||||||
|
);
|
||||||
|
|
||||||
|
// CT-05: 감사 로그
|
||||||
|
helper('audit');
|
||||||
|
if ($sourceOrder) {
|
||||||
|
audit_log(
|
||||||
|
'update',
|
||||||
|
'bag_order',
|
||||||
|
$boIdx,
|
||||||
|
['bo_idx' => (int) $sourceOrder->bo_idx, 'bo_hash' => $beforeHash, 'bo_version' => (int) $sourceOrder->bo_version],
|
||||||
|
array_merge($orderData, ['bo_idx' => $boIdx, 'bo_hash' => $orderHash, 'items' => $hashItems, 'seed_file' => $seedFilePath])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
audit_log('create', 'bag_order', $boIdx, null, array_merge($orderData, ['bo_idx' => $boIdx, 'bo_hash' => $orderHash, 'items' => $hashItems, 'seed_file' => $seedFilePath]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $db->transComplete()) {
|
||||||
|
throw new \RuntimeException('Transaction did not complete');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$db->transRollback();
|
||||||
|
log_message('error', 'BagOrder::store: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->with('error', '발주 저장 중 오류가 발생했습니다. 잠시 후 다시 시도하거나 관리자에게 문의하세요.');
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 등록되었습니다. LOT: ' . $lotNo);
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 등록되었습니다. LOT: ' . $lotNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 효과 지자체(`local_government`)의 행정 구·군 코드(lg_code) */
|
||||||
|
private function resolveGugunCodeFromLg(int $lgIdx): string
|
||||||
|
{
|
||||||
|
$lg = model(LocalGovernmentModel::class)->find($lgIdx);
|
||||||
|
|
||||||
|
return $lg ? trim((string) ($lg->lg_code ?? '')) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateUuidV4(): string
|
||||||
|
{
|
||||||
|
$bytes = random_bytes(16);
|
||||||
|
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||||
|
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||||
|
|
||||||
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateLotNo6(): string
|
||||||
|
{
|
||||||
|
// 문서의 "LOT 번호 6 Byte" 요구를 맞추기 위해 영숫자 6자리로 생성한다.
|
||||||
|
// 충돌 가능성을 낮추기 위해 최대 20회 재시도 후 timestamp 기반으로 fallback.
|
||||||
|
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
for ($attempt = 0; $attempt < 20; $attempt++) {
|
||||||
|
$lot = '';
|
||||||
|
for ($i = 0; $i < 6; $i++) {
|
||||||
|
$lot .= $chars[random_int(0, strlen($chars) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = $this->orderModel->where('bo_lot_no', $lot)->countAllResults() > 0;
|
||||||
|
if (! $exists) {
|
||||||
|
return $lot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtoupper(substr(base_convert((string) time(), 10, 36), -6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $orderData
|
||||||
|
* @param array<int,array<string,mixed>> $items
|
||||||
|
*/
|
||||||
|
private function generateBarcodeSeedFile(string $uuid, int $version, string $lotNo, array $orderData, array $items, string $orderHash): string
|
||||||
|
{
|
||||||
|
$baseDir = WRITEPATH . 'barcode-seeds';
|
||||||
|
if (! is_dir($baseDir)) {
|
||||||
|
mkdir($baseDir, 0775, true);
|
||||||
|
}
|
||||||
|
$keyDir = WRITEPATH . 'keys';
|
||||||
|
if (! is_dir($keyDir)) {
|
||||||
|
mkdir($keyDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_private.pem';
|
||||||
|
$publicKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_public.pem';
|
||||||
|
if (! is_file($privateKeyPath) || ! is_file($publicKeyPath)) {
|
||||||
|
$config = [
|
||||||
|
'private_key_bits' => 2048,
|
||||||
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||||
|
];
|
||||||
|
$resource = openssl_pkey_new($config);
|
||||||
|
if ($resource !== false) {
|
||||||
|
$privatePem = '';
|
||||||
|
openssl_pkey_export($resource, $privatePem);
|
||||||
|
$details = openssl_pkey_get_details($resource);
|
||||||
|
$publicPem = $details['key'] ?? '';
|
||||||
|
if ($privatePem !== '' && $publicPem !== '') {
|
||||||
|
file_put_contents($privateKeyPath, $privatePem);
|
||||||
|
file_put_contents($publicKeyPath, $publicPem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'version' => $version,
|
||||||
|
'lot_no' => $lotNo,
|
||||||
|
'order_hash' => $orderHash,
|
||||||
|
'order' => $orderData,
|
||||||
|
'items' => $items,
|
||||||
|
];
|
||||||
|
$payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}';
|
||||||
|
|
||||||
|
$aesKey = random_bytes(32);
|
||||||
|
$iv = random_bytes(16);
|
||||||
|
$cipherRaw = openssl_encrypt($payloadJson, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
|
||||||
|
if ($cipherRaw === false) {
|
||||||
|
$cipherRaw = $payloadJson;
|
||||||
|
}
|
||||||
|
$encryptedKey = '';
|
||||||
|
$publicPem = is_file($publicKeyPath) ? file_get_contents($publicKeyPath) : '';
|
||||||
|
if (is_string($publicPem) && $publicPem !== '') {
|
||||||
|
openssl_public_encrypt($aesKey, $encryptedKey, $publicPem, OPENSSL_PKCS1_OAEP_PADDING);
|
||||||
|
}
|
||||||
|
|
||||||
|
$seed = [
|
||||||
|
'algorithm' => ['symmetric' => 'AES-256-CBC', 'asymmetric' => 'RSA-2048'],
|
||||||
|
'lot_no' => $lotNo,
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'version' => $version,
|
||||||
|
'iv_b64' => base64_encode($iv),
|
||||||
|
'key_b64' => $encryptedKey !== '' ? base64_encode($encryptedKey) : '',
|
||||||
|
'cipher_b64' => base64_encode((string) $cipherRaw),
|
||||||
|
'payload_hash' => hash('sha256', $payloadJson),
|
||||||
|
'created_at' => date('c'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$fileName = $lotNo . '_v' . $version . '.seed.json';
|
||||||
|
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $fileName;
|
||||||
|
file_put_contents($fullPath, json_encode($seed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
|
return $fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
public function detail(int $id)
|
public function detail(int $id)
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
@@ -250,9 +815,11 @@ class BagOrder extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$before = (array) $order;
|
$before = (array) $order;
|
||||||
$this->orderModel->update($id, ['bo_status' => 'cancelled', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
$beforeHash = (string) ($order->bo_hash ?? '');
|
||||||
|
$this->appendLedgerForStatusChange($order, $id, 'ORDER_CANCEL', 'cancelled', $beforeHash);
|
||||||
|
$after = (array) $this->orderModel->find($id);
|
||||||
helper('audit');
|
helper('audit');
|
||||||
audit_log('update', 'bag_order', $id, $before, ['bo_status' => 'cancelled']);
|
audit_log('update', 'bag_order', $id, $before, $after);
|
||||||
|
|
||||||
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 취소되었습니다.');
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 취소되었습니다.');
|
||||||
}
|
}
|
||||||
@@ -266,10 +833,117 @@ class BagOrder extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$before = (array) $order;
|
$before = (array) $order;
|
||||||
$this->orderModel->update($id, ['bo_status' => 'deleted', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
$beforeHash = (string) ($order->bo_hash ?? '');
|
||||||
|
$this->appendLedgerForStatusChange($order, $id, 'ORDER_DELETE', 'deleted', $beforeHash);
|
||||||
|
$after = (array) $this->orderModel->find($id);
|
||||||
helper('audit');
|
helper('audit');
|
||||||
audit_log('delete', 'bag_order', $id, $before, ['bo_status' => 'deleted']);
|
audit_log('delete', 'bag_order', $id, $before, $after);
|
||||||
|
|
||||||
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 변경 시(취소/삭제) 무결성 검증을 위해 bo_hash 재계산 후
|
||||||
|
* SQL-Ledger(append-only)에 블록을 추가한다.
|
||||||
|
*
|
||||||
|
* @param object $order
|
||||||
|
* @param int $boIdx
|
||||||
|
* @param string $txType ORDER_CANCEL|ORDER_DELETE
|
||||||
|
* @param string $newStatus cancelled|deleted
|
||||||
|
* @param string $previousHash
|
||||||
|
*/
|
||||||
|
private function appendLedgerForStatusChange(object $order, int $boIdx, string $txType, string $newStatus, string $previousHash): void
|
||||||
|
{
|
||||||
|
// 품목은 상태 변경 시 그대로이므로, 동일 payload 형태로 items array를 만든다.
|
||||||
|
$items = $this->itemModel->where('boi_bo_idx', $boIdx)->findAll();
|
||||||
|
$hashItems = [];
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$hashItems[] = [
|
||||||
|
'boi_bo_idx' => (int) $it->boi_bo_idx,
|
||||||
|
'boi_bag_code' => (string) $it->boi_bag_code,
|
||||||
|
'boi_bag_name' => (string) ($it->boi_bag_name ?? ''),
|
||||||
|
'boi_unit_price' => (float) $it->boi_unit_price,
|
||||||
|
'boi_qty_box' => (int) $it->boi_qty_box,
|
||||||
|
'boi_qty_sheet' => (int) $it->boi_qty_sheet,
|
||||||
|
'boi_amount' => (float) $it->boi_amount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$newOrder = $order;
|
||||||
|
$newOrder->bo_status = $newStatus;
|
||||||
|
|
||||||
|
$newHash = $this->computeOrderHash($boIdx, $newOrder, $hashItems);
|
||||||
|
$actorIdx = session()->get('mb_idx') ? (int) session()->get('mb_idx') : null;
|
||||||
|
$lgIdx = (int) ($order->bo_lg_idx ?? 0);
|
||||||
|
|
||||||
|
$seedFilePath = '';
|
||||||
|
$ledgerPayload = [
|
||||||
|
'bo_idx' => $boIdx,
|
||||||
|
'bo_uuid' => (string) $order->bo_uuid,
|
||||||
|
'bo_version' => (int) $order->bo_version,
|
||||||
|
'bo_lot_no' => (string) $order->bo_lot_no,
|
||||||
|
'bo_hash' => $newHash,
|
||||||
|
'seed_file' => $seedFilePath,
|
||||||
|
'hash_chain' => [$previousHash, $newHash],
|
||||||
|
'order' => [
|
||||||
|
'bo_status' => $newStatus,
|
||||||
|
'bo_hash' => $newHash,
|
||||||
|
],
|
||||||
|
'items' => $hashItems,
|
||||||
|
];
|
||||||
|
|
||||||
|
$ledger = new SqlLedger();
|
||||||
|
$ledger->appendBlock(
|
||||||
|
$txType,
|
||||||
|
$ledgerPayload,
|
||||||
|
(string) $order->bo_uuid,
|
||||||
|
(int) $order->bo_version,
|
||||||
|
$actorIdx,
|
||||||
|
$lgIdx
|
||||||
|
);
|
||||||
|
|
||||||
|
// order row에 hash 반영
|
||||||
|
$this->orderModel->update($boIdx, [
|
||||||
|
'bo_status' => $newStatus,
|
||||||
|
'bo_moddate' => date('Y-m-d H:i:s'),
|
||||||
|
'bo_hash' => $newHash,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* store()에서 생성하는 bo_hash와 동일한 "헤더+items" 규격을 사용해 SHA-256을 계산한다.
|
||||||
|
*
|
||||||
|
* @param int $boIdx
|
||||||
|
* @param object $order
|
||||||
|
* @param array<int,array<string,mixed>> $hashItems
|
||||||
|
*/
|
||||||
|
private function computeOrderHash(int $boIdx, object $order, array $hashItems): string
|
||||||
|
{
|
||||||
|
$orderData = [
|
||||||
|
'bo_uuid' => (string) $order->bo_uuid,
|
||||||
|
'bo_version' => (int) $order->bo_version,
|
||||||
|
'bo_lg_idx' => (int) $order->bo_lg_idx,
|
||||||
|
'bo_gugun_code' => (string) ($order->bo_gugun_code ?? ''),
|
||||||
|
'bo_dong_code' => (string) ($order->bo_dong_code ?? ''),
|
||||||
|
'bo_company_idx' => $order->bo_company_idx !== null ? (int) $order->bo_company_idx : null,
|
||||||
|
'bo_agency_idx' => $order->bo_agency_idx !== null ? (int) $order->bo_agency_idx : null,
|
||||||
|
'bo_fee_rate' => (float) ($order->bo_fee_rate ?? 0),
|
||||||
|
'bo_order_date' => (string) $order->bo_order_date,
|
||||||
|
'bo_bag_types' => (string) ($order->bo_bag_types ?? ''),
|
||||||
|
'bo_unit_prices' => (string) ($order->bo_unit_prices ?? ''),
|
||||||
|
'bo_qty_boxes' => (string) ($order->bo_qty_boxes ?? ''),
|
||||||
|
'bo_lot_no' => (string) $order->bo_lot_no,
|
||||||
|
'bo_hash' => '',
|
||||||
|
'bo_status' => (string) $order->bo_status,
|
||||||
|
'bo_orderer_idx' => $order->bo_orderer_idx !== null ? (int) $order->bo_orderer_idx : null,
|
||||||
|
'bo_regdate' => (string) ($order->bo_regdate ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
$hashPayload = $orderData;
|
||||||
|
$hashPayload['bo_idx'] = $boIdx;
|
||||||
|
$hashPayload['items'] = $hashItems;
|
||||||
|
|
||||||
|
$hashJson = json_encode($hashPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
return hash('sha256', $hashJson !== false ? $hashJson : (string) $boIdx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,143 @@ class BagPrice extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $this->priceModel->where('bp_lg_idx', $lgIdx)
|
$get = $this->request->getGet();
|
||||||
|
$readSrc = static function (array $src, string $key): ?string {
|
||||||
|
if (! array_key_exists($key, $src)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$v = $src[$key];
|
||||||
|
if ($v === null || is_array($v)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$s = trim((string) $v);
|
||||||
|
|
||||||
|
return $s === '' ? null : $s;
|
||||||
|
};
|
||||||
|
|
||||||
|
$sy = $readSrc($get, 'start_y');
|
||||||
|
$sm = $readSrc($get, 'start_m');
|
||||||
|
$sd = $readSrc($get, 'start_d');
|
||||||
|
$ey = $readSrc($get, 'end_y');
|
||||||
|
$em = $readSrc($get, 'end_m');
|
||||||
|
$ed = $readSrc($get, 'end_d');
|
||||||
|
|
||||||
|
$startDate = null;
|
||||||
|
if ($sy !== null && $sy !== '' && $sm !== null && $sm !== '' && $sd !== null && $sd !== '') {
|
||||||
|
$startDate = parse_ymd_from_triple($sy, $sm, $sd);
|
||||||
|
}
|
||||||
|
if ($startDate === null) {
|
||||||
|
$legacyStart = $readSrc($get, 'start_date');
|
||||||
|
$startDate = ($legacyStart !== null && $legacyStart !== '') ? $legacyStart : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endDate = null;
|
||||||
|
if ($ey !== null && $ey !== '' && $em !== null && $em !== '' && $ed !== null && $ed !== '') {
|
||||||
|
$endDate = parse_ymd_from_triple($ey, $em, $ed);
|
||||||
|
}
|
||||||
|
if ($endDate === null) {
|
||||||
|
$legacyEnd = $readSrc($get, 'end_date');
|
||||||
|
$endDate = ($legacyEnd !== null && $legacyEnd !== '') ? $legacyEnd : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startParts = ['y' => '', 'm' => '', 'd' => ''];
|
||||||
|
$endParts = ['y' => '', 'm' => '', 'd' => ''];
|
||||||
|
if ($startDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $startDate, $m)) {
|
||||||
|
$startParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]];
|
||||||
|
}
|
||||||
|
if ($endDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $endDate, $m)) {
|
||||||
|
$endParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bagKindE = $readSrc($get, 'bag_kind_e');
|
||||||
|
$bagCode = $readSrc($get, 'bag_code');
|
||||||
|
|
||||||
|
$builder = $this->priceModel->where('bp_lg_idx', $lgIdx);
|
||||||
|
if (($startDate !== null && $startDate !== '') || ($endDate !== null && $endDate !== '')) {
|
||||||
|
$qStart = ($startDate !== null && $startDate !== '') ? $startDate : $endDate;
|
||||||
|
$qEnd = ($endDate !== null && $endDate !== '') ? $endDate : $startDate;
|
||||||
|
if (strcmp((string) $qStart, (string) $qEnd) > 0) {
|
||||||
|
[$qStart, $qEnd] = [$qEnd, $qStart];
|
||||||
|
}
|
||||||
|
$builder->where('bp_start_date <=', $qEnd);
|
||||||
|
$builder->groupStart()
|
||||||
|
->where('bp_end_date IS NULL')
|
||||||
|
->orWhere('bp_end_date >=', $qStart)
|
||||||
|
->groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bagKindE !== null && $bagKindE !== '') {
|
||||||
|
$kindE = model(CodeKindModel::class)->where('ck_code', 'E')->first();
|
||||||
|
if ($kindE) {
|
||||||
|
$detailE = model(CodeDetailModel::class)
|
||||||
|
->where('cd_ck_idx', (int) $kindE->ck_idx)
|
||||||
|
->where('cd_code', $bagKindE)
|
||||||
|
->where('cd_state', 1)
|
||||||
|
->first();
|
||||||
|
if ($detailE !== null) {
|
||||||
|
$builder->like('bp_bag_code', $bagKindE, 'after');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bagCode !== null && $bagCode !== '') {
|
||||||
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
if ($kindO) {
|
||||||
|
$detailO = model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $bagCode, $lgIdx);
|
||||||
|
if ($detailO !== null) {
|
||||||
|
$builder->where('bp_bag_code', $bagCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $builder
|
||||||
->orderBy('bp_bag_code', 'ASC')
|
->orderBy('bp_bag_code', 'ASC')
|
||||||
->orderBy('bp_start_date', 'DESC')
|
->orderBy('bp_start_date', 'DESC')
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
|
$queryForPager = [];
|
||||||
|
if ($sy !== null && $sm !== null && $sd !== null && $sy !== '' && $sm !== '' && $sd !== '') {
|
||||||
|
$queryForPager['start_y'] = $sy;
|
||||||
|
$queryForPager['start_m'] = $sm;
|
||||||
|
$queryForPager['start_d'] = $sd;
|
||||||
|
}
|
||||||
|
if ($ey !== null && $em !== null && $ed !== null && $ey !== '' && $em !== '' && $ed !== '') {
|
||||||
|
$queryForPager['end_y'] = $ey;
|
||||||
|
$queryForPager['end_m'] = $em;
|
||||||
|
$queryForPager['end_d'] = $ed;
|
||||||
|
}
|
||||||
|
if ($bagKindE !== null && $bagKindE !== '') {
|
||||||
|
$queryForPager['bag_kind_e'] = $bagKindE;
|
||||||
|
}
|
||||||
|
if ($bagCode !== null && $bagCode !== '') {
|
||||||
|
$queryForPager['bag_code'] = $bagCode;
|
||||||
|
}
|
||||||
|
$pagerPath = mgmt_url('bag-prices');
|
||||||
|
if ($queryForPager !== []) {
|
||||||
|
$pagerPath .= '?' . http_build_query($queryForPager);
|
||||||
|
}
|
||||||
|
$this->priceModel->pager->setPath($pagerPath);
|
||||||
|
|
||||||
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
|
$bagCodes = $kindO
|
||||||
|
? model(CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx)
|
||||||
|
: [];
|
||||||
|
$kindE = model(CodeKindModel::class)->where('ck_code', 'E')->first();
|
||||||
|
$bagKindOptions = $kindE
|
||||||
|
? model(CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null)
|
||||||
|
: [];
|
||||||
|
|
||||||
return $this->renderWorkPage('봉투 단가 관리', 'admin/bag_price/index', [
|
return $this->renderWorkPage('봉투 단가 관리', 'admin/bag_price/index', [
|
||||||
'list' => $list,
|
'list' => $list,
|
||||||
'pager' => $this->priceModel->pager,
|
'pager' => $this->priceModel->pager,
|
||||||
|
'startParts' => $startParts,
|
||||||
|
'endParts' => $endParts,
|
||||||
|
'dateYearMin' => (int) date('Y') - 12,
|
||||||
|
'dateYearMax' => (int) date('Y') + 2,
|
||||||
|
'bag_kind_e' => $bagKindE,
|
||||||
|
'bag_code' => $bagCode,
|
||||||
|
'bag_codes' => $bagCodes,
|
||||||
|
'bag_kind_options' => $bagKindOptions,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class BagSale extends BaseController
|
|||||||
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
||||||
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
|
||||||
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $bagCode)->where('bp_state', 1)->first();
|
$price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, (string) $bagCode);
|
||||||
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
||||||
|
|
||||||
$actualQty = ($type === 'return') ? -$qty : $qty;
|
$actualQty = ($type === 'return') ? -$qty : $qty;
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ class Company extends BaseController
|
|||||||
{
|
{
|
||||||
private CompanyModel $model;
|
private CompanyModel $model;
|
||||||
|
|
||||||
|
private function companyTypeOptions(): array
|
||||||
|
{
|
||||||
|
return ['협회', '제작업체', '회수업체'];
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->model = model(CompanyModel::class);
|
$this->model = model(CompanyModel::class);
|
||||||
@@ -22,10 +27,33 @@ class Company extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->paginate(20);
|
$companyType = trim((string) ($this->request->getGet('cp_type') ?? ''));
|
||||||
|
$typeOptions = $this->companyTypeOptions();
|
||||||
|
|
||||||
|
$builder = $this->model->where('cp_lg_idx', $lgIdx);
|
||||||
|
if ($companyType !== '' && in_array($companyType, $typeOptions, true)) {
|
||||||
|
$builder->where('cp_type', $companyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $builder->orderBy('cp_idx', 'DESC')->paginate(20);
|
||||||
$pager = $this->model->pager;
|
$pager = $this->model->pager;
|
||||||
|
|
||||||
return $this->renderWorkPage('업체 관리', 'admin/company/index', ['list' => $list, 'pager' => $pager]);
|
$queryForPager = [];
|
||||||
|
if ($companyType !== '' && in_array($companyType, $typeOptions, true)) {
|
||||||
|
$queryForPager['cp_type'] = $companyType;
|
||||||
|
}
|
||||||
|
$pagerPath = mgmt_url('companies');
|
||||||
|
if ($queryForPager !== []) {
|
||||||
|
$pagerPath .= '?' . http_build_query($queryForPager);
|
||||||
|
}
|
||||||
|
$pager->setPath($pagerPath);
|
||||||
|
|
||||||
|
return $this->renderWorkPage('업체 관리', 'admin/company/index', [
|
||||||
|
'list' => $list,
|
||||||
|
'pager' => $pager,
|
||||||
|
'cpType' => $companyType,
|
||||||
|
'typeOptions' => $typeOptions,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,15 @@ class Manager extends BaseController
|
|||||||
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function managerCategoryOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'company' => '제작업체',
|
||||||
|
'district' => '구·군',
|
||||||
|
'agency' => '대행소',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
@@ -35,16 +44,29 @@ class Manager extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->paginate(20);
|
$category = (string) ($this->request->getGet('category') ?? '');
|
||||||
|
$categories = $this->managerCategoryOptions();
|
||||||
|
|
||||||
|
$builder = $this->model->where('mg_lg_idx', $lgIdx);
|
||||||
|
if ($category !== '' && isset($categories[$category])) {
|
||||||
|
$builder->where('mg_dept_code', $category);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $builder->orderBy('mg_idx', 'DESC')->paginate(20);
|
||||||
$pager = $this->model->pager;
|
$pager = $this->model->pager;
|
||||||
|
|
||||||
return $this->renderWorkPage('담당자 관리', 'admin/manager/index', ['list' => $list, 'pager' => $pager]);
|
return $this->renderWorkPage('담당자 관리', 'admin/manager/index', [
|
||||||
|
'list' => $list,
|
||||||
|
'pager' => $pager,
|
||||||
|
'categories' => $categories,
|
||||||
|
'category' => $category,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
return $this->renderWorkPage('담당자 등록', 'admin/manager/create', [
|
return $this->renderWorkPage('담당자 등록', 'admin/manager/create', [
|
||||||
'deptCodes' => $this->getCodeOptions('S'),
|
'categories' => $this->managerCategoryOptions(),
|
||||||
'positionCodes' => $this->getCodeOptions('T'),
|
'positionCodes' => $this->getCodeOptions('T'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -54,6 +76,7 @@ class Manager extends BaseController
|
|||||||
helper(['admin', 'url']);
|
helper(['admin', 'url']);
|
||||||
$rules = [
|
$rules = [
|
||||||
'mg_name' => 'required|max_length[50]',
|
'mg_name' => 'required|max_length[50]',
|
||||||
|
'mg_category' => 'required|in_list[company,district,agency]',
|
||||||
'mg_tel' => 'permit_empty|max_length[20]',
|
'mg_tel' => 'permit_empty|max_length[20]',
|
||||||
'mg_phone' => 'permit_empty|max_length[20]',
|
'mg_phone' => 'permit_empty|max_length[20]',
|
||||||
'mg_email' => 'permit_empty|valid_email|max_length[100]',
|
'mg_email' => 'permit_empty|valid_email|max_length[100]',
|
||||||
@@ -65,7 +88,7 @@ class Manager extends BaseController
|
|||||||
$this->model->insert([
|
$this->model->insert([
|
||||||
'mg_lg_idx' => admin_effective_lg_idx(),
|
'mg_lg_idx' => admin_effective_lg_idx(),
|
||||||
'mg_name' => $this->request->getPost('mg_name'),
|
'mg_name' => $this->request->getPost('mg_name'),
|
||||||
'mg_dept_code' => $this->request->getPost('mg_dept_code') ?? '',
|
'mg_dept_code' => (string) ($this->request->getPost('mg_category') ?? ''),
|
||||||
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
||||||
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
||||||
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
||||||
@@ -87,7 +110,7 @@ class Manager extends BaseController
|
|||||||
|
|
||||||
return $this->renderWorkPage('담당자 수정', 'admin/manager/edit', [
|
return $this->renderWorkPage('담당자 수정', 'admin/manager/edit', [
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'deptCodes' => $this->getCodeOptions('S'),
|
'categories' => $this->managerCategoryOptions(),
|
||||||
'positionCodes' => $this->getCodeOptions('T'),
|
'positionCodes' => $this->getCodeOptions('T'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -102,6 +125,7 @@ class Manager extends BaseController
|
|||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
'mg_name' => 'required|max_length[50]',
|
'mg_name' => 'required|max_length[50]',
|
||||||
|
'mg_category' => 'required|in_list[company,district,agency]',
|
||||||
'mg_state' => 'required|in_list[0,1]',
|
'mg_state' => 'required|in_list[0,1]',
|
||||||
];
|
];
|
||||||
if (! $this->validate($rules)) {
|
if (! $this->validate($rules)) {
|
||||||
@@ -110,7 +134,7 @@ class Manager extends BaseController
|
|||||||
|
|
||||||
$this->model->update($id, [
|
$this->model->update($id, [
|
||||||
'mg_name' => $this->request->getPost('mg_name'),
|
'mg_name' => $this->request->getPost('mg_name'),
|
||||||
'mg_dept_code' => $this->request->getPost('mg_dept_code') ?? '',
|
'mg_dept_code' => (string) ($this->request->getPost('mg_category') ?? ''),
|
||||||
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
||||||
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
||||||
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
||||||
|
|||||||
@@ -143,14 +143,31 @@ class PackagingUnit extends BaseController
|
|||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$db->transStart();
|
$db->transStart();
|
||||||
|
|
||||||
$trackFields = ['pu_box_per_pack', 'pu_pack_per_sheet'];
|
$trackFields = ['pu_box_per_pack', 'pu_pack_per_sheet', 'pu_start_date', 'pu_end_date', 'pu_state'];
|
||||||
|
$fieldLabels = [
|
||||||
|
'pu_box_per_pack' => '박스당 팩 수',
|
||||||
|
'pu_pack_per_sheet' => '팩당 낱장 수',
|
||||||
|
'pu_start_date' => '적용시작일',
|
||||||
|
'pu_end_date' => '적용종료일',
|
||||||
|
'pu_state' => '상태',
|
||||||
|
];
|
||||||
foreach ($trackFields as $field) {
|
foreach ($trackFields as $field) {
|
||||||
$oldVal = (string) $item->$field;
|
$oldRaw = $item->$field;
|
||||||
$newVal = (string) $this->request->getPost($field);
|
$newRaw = $this->request->getPost($field);
|
||||||
|
if ($field === 'pu_end_date') {
|
||||||
|
$oldRaw = $oldRaw ?: '';
|
||||||
|
$newRaw = $newRaw ?: '';
|
||||||
|
}
|
||||||
|
if ($field === 'pu_state') {
|
||||||
|
$oldRaw = (int) $oldRaw === 1 ? '사용' : '미사용';
|
||||||
|
$newRaw = (int) $newRaw === 1 ? '사용' : '미사용';
|
||||||
|
}
|
||||||
|
$oldVal = (string) $oldRaw;
|
||||||
|
$newVal = (string) $newRaw;
|
||||||
if ($oldVal !== $newVal) {
|
if ($oldVal !== $newVal) {
|
||||||
$this->historyModel->insert([
|
$this->historyModel->insert([
|
||||||
'puh_pu_idx' => $id,
|
'puh_pu_idx' => $id,
|
||||||
'puh_field' => $field,
|
'puh_field' => $fieldLabels[$field] ?? $field,
|
||||||
'puh_old_value' => $oldVal,
|
'puh_old_value' => $oldVal,
|
||||||
'puh_new_value' => $newVal,
|
'puh_new_value' => $newVal,
|
||||||
'puh_changed_at' => date('Y-m-d H:i:s'),
|
'puh_changed_at' => date('Y-m-d H:i:s'),
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class ShopOrder extends BaseController
|
|||||||
}
|
}
|
||||||
$qty = (int) $qtys[$i];
|
$qty = (int) $qtys[$i];
|
||||||
|
|
||||||
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $code)->where('bp_state', 1)->first();
|
$price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, (string) $code);
|
||||||
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
||||||
$amount = $unitPrice * $qty;
|
$amount = $unitPrice * $qty;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -51,13 +51,16 @@ abstract class BaseController extends Controller
|
|||||||
protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string
|
protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string
|
||||||
{
|
{
|
||||||
$content = view($contentView, $contentData);
|
$content = view($contentView, $contentData);
|
||||||
$uri = service('request')->getUri();
|
helper('admin');
|
||||||
$seg1 = $uri->getSegment(1);
|
$path = function_exists('current_nav_request_path') ? current_nav_request_path() : '';
|
||||||
$seg2 = $uri->getSegment(2);
|
if ($path === '') {
|
||||||
|
$uri = service('request')->getUri();
|
||||||
// 지정판매소 관리는 관리자 전용 기능으로, /bag 경로여도 관리자 레이아웃을 유지한다.
|
$path = trim((string) $uri->getPath(), '/');
|
||||||
$forceAdminLayoutOnBag = ($seg1 === 'bag' && $seg2 === 'designated-shops');
|
}
|
||||||
if ($seg1 === 'bag' && ! $forceAdminLayoutOnBag) {
|
while (str_starts_with($path, 'index.php/')) {
|
||||||
|
$path = substr($path, strlen('index.php/'));
|
||||||
|
}
|
||||||
|
if ($path === 'bag' || str_starts_with($path, 'bag/')) {
|
||||||
return view('bag/layout/main', [
|
return view('bag/layout/main', [
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'content' => $content,
|
'content' => $content,
|
||||||
|
|||||||
@@ -475,6 +475,68 @@ if (! function_exists('site_nav_link_matches_current')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('menu_active_child_for_parent')) {
|
||||||
|
/**
|
||||||
|
* 같은 부모 아래 형제 소메뉴 중, 현재 요청에 해당하는 항목을 하나만 고른다.
|
||||||
|
*
|
||||||
|
* 짧은 mm_link(예: bag/designated-shops)가 긴 경로(bag/designated-shops/browse)와
|
||||||
|
* 동시에 prefix 규칙으로 매칭될 때, 가장 구체적인 경로(일치한 후보 문자열 길이 최대)만 활성으로 본다.
|
||||||
|
* 길이가 같으면 mm_num이 작은 항목을 선택(동일 URL이 여러 메뉴에 매핑된 경우 등).
|
||||||
|
*
|
||||||
|
* @param object{children?: array<int, object>} $parentNavItem
|
||||||
|
* @param list<string> $dashboardPathAliases
|
||||||
|
*
|
||||||
|
* @return object|null 활성으로 표시할 자식 노드(mm_idx 등 포함), 없으면 null
|
||||||
|
*/
|
||||||
|
function menu_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
|
||||||
|
{
|
||||||
|
$children = $parentNavItem->children ?? [];
|
||||||
|
if ($children === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$best = null;
|
||||||
|
$bestLen = -1;
|
||||||
|
$bestMmNum = PHP_INT_MAX;
|
||||||
|
|
||||||
|
foreach ($children as $child) {
|
||||||
|
$mmLink = $child->mm_link ?? null;
|
||||||
|
$maxLen = -1;
|
||||||
|
foreach (menu_link_candidate_paths($mmLink, $currentPath) as $cand) {
|
||||||
|
if (menu_single_path_matches_request($cand, $currentPath, $dashboardPathAliases)) {
|
||||||
|
$maxLen = max($maxLen, strlen($cand));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($maxLen < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$mmNum = (int) ($child->mm_num ?? 0);
|
||||||
|
if ($maxLen > $bestLen || ($maxLen === $bestLen && $mmNum < $bestMmNum)) {
|
||||||
|
$bestLen = $maxLen;
|
||||||
|
$bestMmNum = $mmNum;
|
||||||
|
$best = $child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('site_nav_active_child_for_parent')) {
|
||||||
|
/**
|
||||||
|
* 사이트 상단 메뉴 전용 호환 래퍼.
|
||||||
|
*
|
||||||
|
* @param object{children?: array<int, object>} $parentNavItem
|
||||||
|
* @param list<string> $dashboardPathAliases
|
||||||
|
*
|
||||||
|
* @return object|null
|
||||||
|
*/
|
||||||
|
function site_nav_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
|
||||||
|
{
|
||||||
|
return menu_active_child_for_parent($parentNavItem, $currentPath, $dashboardPathAliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (! function_exists('session_user_nav_display')) {
|
if (! function_exists('session_user_nav_display')) {
|
||||||
/**
|
/**
|
||||||
* 상단 메뉴바용: 로그인 사용자 이름·역할 표시
|
* 상단 메뉴바용: 로그인 사용자 이름·역할 표시
|
||||||
|
|||||||
@@ -16,4 +16,45 @@ class BagPriceModel extends Model
|
|||||||
'bp_start_date', 'bp_end_date', 'bp_state',
|
'bp_start_date', 'bp_end_date', 'bp_state',
|
||||||
'bp_regdate', 'bp_moddate', 'bp_reg_mb_idx',
|
'bp_regdate', 'bp_moddate', 'bp_reg_mb_idx',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 같은 봉투코드에 단가 기간이 겹쳐도 "나중 등록 단가"가 우선되도록
|
||||||
|
* 활성 단가를 등록일/PK 역순으로 정렬해 봉투코드별 1건만 남긴다.
|
||||||
|
*
|
||||||
|
* @return array<string, object>
|
||||||
|
*/
|
||||||
|
public function latestActiveMapByBagCode(int $lgIdx): array
|
||||||
|
{
|
||||||
|
$rows = $this->where('bp_lg_idx', $lgIdx)
|
||||||
|
->where('bp_state', 1)
|
||||||
|
->orderBy('bp_regdate', 'DESC')
|
||||||
|
->orderBy('bp_idx', 'DESC')
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$code = (string) ($row->bp_bag_code ?? '');
|
||||||
|
if ($code === '' || isset($map[$code])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$map[$code] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latestActiveByBagCode(int $lgIdx, string $bagCode): ?object
|
||||||
|
{
|
||||||
|
$bagCode = trim($bagCode);
|
||||||
|
if ($bagCode === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->where('bp_lg_idx', $lgIdx)
|
||||||
|
->where('bp_bag_code', $bagCode)
|
||||||
|
->where('bp_state', 1)
|
||||||
|
->orderBy('bp_regdate', 'DESC')
|
||||||
|
->orderBy('bp_idx', 'DESC')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,25 @@ class DesignatedShopModel extends Model
|
|||||||
'ds_name',
|
'ds_name',
|
||||||
'ds_biz_no',
|
'ds_biz_no',
|
||||||
'ds_rep_name',
|
'ds_rep_name',
|
||||||
|
'ds_biz_type',
|
||||||
|
'ds_biz_kind',
|
||||||
'ds_va_number',
|
'ds_va_number',
|
||||||
|
'ds_va_bank',
|
||||||
|
'ds_va_account',
|
||||||
'ds_zip',
|
'ds_zip',
|
||||||
'ds_addr',
|
'ds_addr',
|
||||||
'ds_addr_jibun',
|
'ds_addr_jibun',
|
||||||
|
'ds_addr_detail',
|
||||||
'ds_tel',
|
'ds_tel',
|
||||||
'ds_rep_phone',
|
'ds_rep_phone',
|
||||||
'ds_email',
|
'ds_email',
|
||||||
'ds_gugun_code',
|
'ds_gugun_code',
|
||||||
|
'ds_zone_code',
|
||||||
|
'ds_branch_no',
|
||||||
'ds_designated_at',
|
'ds_designated_at',
|
||||||
'ds_state',
|
'ds_state',
|
||||||
|
'ds_state_changed_at',
|
||||||
|
'ds_change_reason',
|
||||||
'ds_regdate',
|
'ds_regdate',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?= view('components/print_header', ['printTitle' => '봉투 단가 관리']) ?>
|
<?= view('components/print_header', ['printTitle' => '봉투 단가 관리', 'printShowApproval' => false]) ?>
|
||||||
<style>
|
<style>
|
||||||
@media print {
|
@media print {
|
||||||
.no-print { display: none !important; }
|
.no-print { display: none !important; }
|
||||||
@@ -9,12 +9,91 @@
|
|||||||
<span class="text-sm font-bold text-gray-700">봉투 단가 관리</span>
|
<span class="text-sm font-bold text-gray-700">봉투 단가 관리</span>
|
||||||
<div class="flex items-center gap-2 no-print">
|
<div class="flex items-center gap-2 no-print">
|
||||||
<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>
|
<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="<?= base_url('bag/prices') ?>" class="text-blue-600 hover:underline text-sm">단가 조회·검색</a>
|
|
||||||
<a href="<?= mgmt_url('bag-prices/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">단가 등록</a>
|
<a href="<?= mgmt_url('bag-prices/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">단가 등록</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<p class="text-xs text-gray-500 mt-2 no-print">목록·등록·수정·삭제는 이 화면에서, <strong>기간·봉투별 조회·인쇄</strong>는 <a href="<?= base_url('bag/prices') ?>" class="text-blue-600 hover:underline">봉투 단가(조회)</a>에서 이용하세요.</p>
|
<section class="no-print border border-gray-200 rounded-lg bg-white p-3 mt-2">
|
||||||
|
<form method="get" action="<?= mgmt_url('bag-prices') ?>" class="flex flex-wrap items-end gap-3" autocomplete="off">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<label class="text-xs text-gray-500">봉투구분</label>
|
||||||
|
<select name="bag_kind_e" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[9rem]">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<?php foreach ($bag_kind_options ?? [] as $cd): ?>
|
||||||
|
<option value="<?= esc($cd->cd_code) ?>" <?= (string) ($cd->cd_code ?? '') === (string) ($bag_kind_e ?? '') ? 'selected' : '' ?>>
|
||||||
|
<?= esc($cd->cd_name) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<label class="text-xs text-gray-500">봉투코드</label>
|
||||||
|
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[11rem]">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<?php foreach ($bag_codes ?? [] as $cd): ?>
|
||||||
|
<option value="<?= esc($cd->cd_code) ?>" <?= (string) ($cd->cd_code ?? '') === (string) ($bag_code ?? '') ? 'selected' : '' ?>>
|
||||||
|
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<label class="text-xs text-gray-500">조회 기간 (적용기간 겹침)</label>
|
||||||
|
<?php
|
||||||
|
$sp = $startParts ?? ['y' => '', 'm' => '', 'd' => ''];
|
||||||
|
$ep = $endParts ?? ['y' => '', 'm' => '', 'd' => ''];
|
||||||
|
$ymin = (int) ($dateYearMin ?? ((int) date('Y') - 12));
|
||||||
|
$ymax = (int) ($dateYearMax ?? ((int) date('Y') + 2));
|
||||||
|
?>
|
||||||
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
|
<span class="text-xs text-gray-500 mr-0.5">시작</span>
|
||||||
|
<select name="start_y" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[4.5rem]">
|
||||||
|
<option value="">연도</option>
|
||||||
|
<?php for ($yy = $ymin; $yy <= $ymax; $yy++): ?>
|
||||||
|
<option value="<?= $yy ?>" <?= (string) ($sp['y'] ?? '') === (string) $yy ? 'selected' : '' ?>><?= $yy ?></option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
<select name="start_m" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
||||||
|
<option value="">월</option>
|
||||||
|
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
|
||||||
|
<option value="<?= $mi ?>" <?= isset($sp['m']) && (int) $sp['m'] === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
<select name="start_d" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
||||||
|
<option value="">일</option>
|
||||||
|
<?php for ($di = 1; $di <= 31; $di++): ?>
|
||||||
|
<option value="<?= $di ?>" <?= isset($sp['d']) && (int) $sp['d'] === $di ? 'selected' : '' ?>><?= $di ?>일</option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
<span class="text-sm text-gray-500 mx-0.5">~</span>
|
||||||
|
<span class="text-xs text-gray-500 mr-0.5">종료</span>
|
||||||
|
<select name="end_y" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[4.5rem]">
|
||||||
|
<option value="">연도</option>
|
||||||
|
<?php for ($yy = $ymin; $yy <= $ymax; $yy++): ?>
|
||||||
|
<option value="<?= $yy ?>" <?= (string) ($ep['y'] ?? '') === (string) $yy ? 'selected' : '' ?>><?= $yy ?></option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
<select name="end_m" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
||||||
|
<option value="">월</option>
|
||||||
|
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
|
||||||
|
<option value="<?= $mi ?>" <?= isset($ep['m']) && (int) $ep['m'] === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
<select name="end_d" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
||||||
|
<option value="">일</option>
|
||||||
|
<?php for ($di = 1; $di <= 31; $di++): ?>
|
||||||
|
<option value="<?= $di ?>" <?= isset($ep['d']) && (int) $ep['d'] === $di ? 'selected' : '' ?>><?= $di ?>일</option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 pb-0.5">
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
||||||
|
<a href="<?= mgmt_url('bag-prices') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
||||||
|
<button type="button" onclick="window.print()" class="border border-gray-300 text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -32,9 +111,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php foreach ($list as $row): ?>
|
<?php
|
||||||
|
$startNo = 1;
|
||||||
|
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
||||||
|
$currentPage = (int) $pager->getCurrentPage();
|
||||||
|
$perPage = (int) $pager->getPerPage();
|
||||||
|
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php foreach (($list ?? []) as $idx => $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->bp_idx) ?></td>
|
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
||||||
<td class="text-center font-mono"><?= esc($row->bp_bag_code) ?></td>
|
<td class="text-center font-mono"><?= esc($row->bp_bag_code) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->bp_bag_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->bp_bag_name) ?></td>
|
||||||
<td><?= number_format((float) $row->bp_order_price) ?></td>
|
<td><?= number_format((float) $row->bp_order_price) ?></td>
|
||||||
|
|||||||
@@ -8,6 +8,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
|
<form method="GET" action="<?= mgmt_url('companies') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-600">업체유형</label>
|
||||||
|
<select name="cp_type" class="border border-gray-300 rounded px-2 py-1 text-sm w-44">
|
||||||
|
<option value="">전 체</option>
|
||||||
|
<?php foreach (($typeOptions ?? []) as $type): ?>
|
||||||
|
<option value="<?= esc($type) ?>" <?= (string) ($cpType ?? '') === (string) $type ? 'selected' : '' ?>><?= esc($type) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
|
<a href="<?= mgmt_url('companies') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -24,9 +37,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($list as $row): ?>
|
<?php
|
||||||
|
$startNo = 1;
|
||||||
|
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
||||||
|
$currentPage = (int) $pager->getCurrentPage();
|
||||||
|
$perPage = (int) $pager->getPerPage();
|
||||||
|
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php foreach (($list ?? []) as $idx => $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->cp_idx) ?></td>
|
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
||||||
<td class="text-center"><?= esc($row->cp_type) ?></td>
|
<td class="text-center"><?= esc($row->cp_type) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->cp_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->cp_name) ?></td>
|
||||||
<td class="text-center"><?= esc($row->cp_biz_no) ?></td>
|
<td class="text-center"><?= esc($row->cp_biz_no) ?></td>
|
||||||
|
|||||||
137
app/Views/admin/designated_shop/barcode.php
Normal file
137
app/Views/admin/designated_shop/barcode.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?= view('components/print_header', ['printTitle' => '지정판매소 바코드 출력']) ?>
|
||||||
|
<style>
|
||||||
|
.ds-bc-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||||
|
.ds-bc-table th, .ds-bc-table td { border: 1px solid #ccc; padding: 4px 6px; }
|
||||||
|
.ds-bc-table th { background: #e9ecef; color: #2d3748; }
|
||||||
|
.ds-bc-table td { background: #fff; }
|
||||||
|
.ds-bc-table td.name-cell { max-width: 14rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.ds-bc-table td.addr-cell { max-width: 24rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.ds-bc-check { width: 14px; height: 14px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
<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">
|
||||||
|
<button type="button" id="ds-bc-print-btn" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">인쇄</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
|
<form id="ds-bc-filter-form" method="get" action="<?= mgmt_url('designated-shops/barcode') ?>" class="flex flex-wrap items-end gap-3">
|
||||||
|
<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 ?? '현재 지자체') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-600 mb-0.5">읍·면·동</label>
|
||||||
|
<select name="ds_zone_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<?php foreach (($zones ?? []) as $z): ?>
|
||||||
|
<?php $zc = trim((string) ($z->zone_code ?? '')); ?>
|
||||||
|
<option value="<?= esc($zc) ?>" <?= ($zoneFilter ?? '') === $zc ? 'selected' : '' ?>><?= esc($zc) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-600 mb-0.5">조회순서</label>
|
||||||
|
<select name="order_by" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[8rem]">
|
||||||
|
<option value="shop_no" <?= ($orderBy ?? 'shop_no') === 'shop_no' ? 'selected' : '' ?>>판매소 코드</option>
|
||||||
|
<option value="name" <?= ($orderBy ?? '') === 'name' ? 'selected' : '' ?>>판매소명</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mx-2 mt-2 mb-2">
|
||||||
|
<form id="ds-bc-print-form" method="post" action="<?= mgmt_url('designated-shops/barcode/print') ?>" target="ds-bc-print-frame">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="zone_label" value="<?= esc(($zoneFilter ?? '') !== '' ? (string) $zoneFilter : '전체') ?>">
|
||||||
|
<div class="mb-1 text-xs text-gray-600">
|
||||||
|
<label class="inline-flex items-center gap-1 cursor-pointer"><input type="checkbox" id="ds-bc-check-all" class="ds-bc-check"> 전체선택</label>
|
||||||
|
<span class="ml-3">선택 건수: <strong id="ds-bc-selected-count">0</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto border border-gray-300 bg-white">
|
||||||
|
<table class="ds-bc-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-14">출력</th>
|
||||||
|
<th class="w-36">판매소 코드</th>
|
||||||
|
<th>판매소명</th>
|
||||||
|
<th class="w-24">대표자명</th>
|
||||||
|
<th class="w-32">사업자번호</th>
|
||||||
|
<th>사업장 주소</th>
|
||||||
|
<th class="w-16">상태</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach (($list ?? []) as $row): ?>
|
||||||
|
<?php
|
||||||
|
$st = (int) ($row->ds_state ?? 1);
|
||||||
|
$stLabel = $st === 1 ? '사용' : '정지';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><input class="ds-bc-row-check ds-bc-check" type="checkbox" name="ds_idx[]" value="<?= (int) $row->ds_idx ?>"></td>
|
||||||
|
<td class="text-center text-blue-700"><?= esc((string) ($row->ds_shop_no ?? '')) ?></td>
|
||||||
|
<td class="name-cell text-blue-700" title="<?= esc((string) ($row->ds_name ?? '')) ?>"><?= esc((string) ($row->ds_name ?? '')) ?></td>
|
||||||
|
<td><?= esc((string) ($row->ds_rep_name ?? '')) ?></td>
|
||||||
|
<td><?= esc((string) ($row->ds_biz_no ?? '')) ?></td>
|
||||||
|
<td class="addr-cell" title="<?= esc((string) ($row->ds_addr ?? '')) ?>"><?= esc((string) ($row->ds_addr ?? '')) ?></td>
|
||||||
|
<td class="<?= $st === 1 ? 'text-blue-700' : 'text-red-600' ?>"><?= esc($stLabel) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($list)): ?>
|
||||||
|
<tr><td colspan="7" class="text-center text-gray-400 py-8">조회된 지정판매소가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<iframe name="ds-bc-print-frame" class="hidden" style="display:none;width:0;height:0;border:0;" aria-hidden="true"></iframe>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php if (isset($pager)): ?>
|
||||||
|
<div class="mt-2 mb-2 mx-2 no-print"><?= $pager->links() ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var all = document.getElementById('ds-bc-check-all');
|
||||||
|
var countEl = document.getElementById('ds-bc-selected-count');
|
||||||
|
var printBtn = document.getElementById('ds-bc-print-btn');
|
||||||
|
var printForm = document.getElementById('ds-bc-print-form');
|
||||||
|
var rows = Array.prototype.slice.call(document.querySelectorAll('.ds-bc-row-check'));
|
||||||
|
if (!all || !countEl || !rows.length) return;
|
||||||
|
|
||||||
|
function refreshCount() {
|
||||||
|
var n = rows.filter(function (el) { return el.checked; }).length;
|
||||||
|
countEl.textContent = String(n);
|
||||||
|
all.checked = n > 0 && n === rows.length;
|
||||||
|
all.indeterminate = n > 0 && n < rows.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
all.addEventListener('change', function () {
|
||||||
|
rows.forEach(function (el) { el.checked = all.checked; });
|
||||||
|
refreshCount();
|
||||||
|
});
|
||||||
|
rows.forEach(function (el) { el.addEventListener('change', refreshCount); });
|
||||||
|
|
||||||
|
if (printBtn && printForm) {
|
||||||
|
printBtn.addEventListener('click', function () {
|
||||||
|
var selected = rows.filter(function (el) { return el.checked; }).length;
|
||||||
|
if (selected < 1) {
|
||||||
|
window.alert('출력할 지정판매소를 1개 이상 선택해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printForm.action = "<?= esc(mgmt_url('designated-shops/barcode/print')) ?>?autoprint=1";
|
||||||
|
printForm.submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCount();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
94
app/Views/admin/designated_shop/barcode_print.php
Normal file
94
app/Views/admin/designated_shop/barcode_print.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
$rows = $rows ?? [];
|
||||||
|
$zoneLabel = trim((string) ($zoneLabel ?? '전체'));
|
||||||
|
$printedAt = trim((string) ($printedAt ?? date('Y.m.d')));
|
||||||
|
$chunks = array_chunk($rows, 12);
|
||||||
|
$totalPages = count($chunks);
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>지정판매소 바코드</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; font-family: Arial, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; color: #222; background: #fff; }
|
||||||
|
.page { width: 210mm; min-height: 297mm; margin: 0 auto; padding: 14mm 12mm 12mm; box-sizing: border-box; }
|
||||||
|
.title { text-align: center; font-size: 42px; letter-spacing: 1px; font-weight: 500; margin: 0 0 14px; }
|
||||||
|
.meta { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #555; padding-bottom: 4px; font-size: 13px; margin-bottom: 8px; }
|
||||||
|
.meta .center { font-weight: 700; }
|
||||||
|
.cards { display: flex; flex-wrap: wrap; align-content: flex-start; }
|
||||||
|
.card { width: 33.3333%; padding: 0 8px 12px; box-sizing: border-box; }
|
||||||
|
.barcode-wrap { min-height: 40px; }
|
||||||
|
.barcode-svg { width: 100%; max-width: 270px; height: 22px; }
|
||||||
|
.code-text { text-align: center; margin-top: 1px; font-size: 16px; letter-spacing: 0.35px; }
|
||||||
|
.name-text { text-align: center; margin-top: 5px; font-size: 14px; line-height: 1.2; word-break: keep-all; }
|
||||||
|
@media print {
|
||||||
|
@page { size: A4; margin: 0; }
|
||||||
|
.page { page-break-after: always; }
|
||||||
|
.page:last-child { page-break-after: auto; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php if ($rows === []): ?>
|
||||||
|
<div class="page">
|
||||||
|
<h1 class="title">지정판매소 바코드</h1>
|
||||||
|
<p style="text-align:center; margin-top:30px; color:#666;">출력할 지정판매소가 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($chunks as $pageIndex => $pageRows): ?>
|
||||||
|
<section class="page">
|
||||||
|
<h1 class="title">지정판매소 바코드</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<span>출 력 일 자: <?= esc($printedAt) ?></span>
|
||||||
|
<span class="center"><?= esc($zoneLabel) ?></span>
|
||||||
|
<span>페 이 지: <?= (int) ($pageIndex + 1) ?> / <?= (int) $totalPages ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="cards">
|
||||||
|
<?php foreach ($pageRows as $row): ?>
|
||||||
|
<?php
|
||||||
|
$code = trim((string) ($row->ds_shop_no ?? ''));
|
||||||
|
$nm = trim((string) ($row->ds_name ?? ''));
|
||||||
|
$rep = trim((string) ($row->ds_rep_name ?? ''));
|
||||||
|
$label = trim($nm . ($rep !== '' ? ('-' . $rep) : ''));
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="barcode-wrap">
|
||||||
|
<svg class="barcode-svg" data-barcode="<?= esc($code, 'attr') ?>"></svg>
|
||||||
|
</div>
|
||||||
|
<div class="code-text"><?= esc($code) ?></div>
|
||||||
|
<div class="name-text"><?= esc($label) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var svgs = document.querySelectorAll('svg[data-barcode]');
|
||||||
|
svgs.forEach(function (svg) {
|
||||||
|
var code = (svg.getAttribute('data-barcode') || '').trim();
|
||||||
|
if (!code) return;
|
||||||
|
try {
|
||||||
|
JsBarcode(svg, code, {
|
||||||
|
format: 'CODE128',
|
||||||
|
displayValue: false,
|
||||||
|
margin: 0,
|
||||||
|
height: 16,
|
||||||
|
width: 1.28
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
svg.outerHTML = '<div style="font-size:12px;color:#b91c1c;">바코드 생성 실패: ' + code + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (window.location.search.indexOf('autoprint=1') >= 0) {
|
||||||
|
setTimeout(function () { window.print(); }, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<span class="text-sm font-bold text-gray-700">지정판매소 등록</span>
|
<span class="text-sm font-bold text-gray-700">지정판매소 등록</span>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
<form action="<?= mgmt_url('designated-shops/store') ?>" method="POST" class="space-y-4">
|
<form id="designated-shop-create-form" action="<?= mgmt_url('designated-shops/store') ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<?php if (! empty($localGovs)): ?>
|
<?php if (! empty($localGovs)): ?>
|
||||||
@@ -23,14 +23,18 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
||||||
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
||||||
|
<span class="text-gray-500 ml-1"><?= esc(trim((string) ($currentLg->lg_sido ?? '') . ' ' . (string) ($currentLg->lg_gugun ?? ''))) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="ds_lg_idx" value="<?= esc($currentLg->lg_idx) ?>"/>
|
<input type="hidden" name="ds_lg_idx" value="<?= esc($currentLg->lg_idx) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<input type="hidden" name="addr_search_sido" id="addr_search_sido" value="<?= esc((string) old('addr_search_sido', '')) ?>"/>
|
||||||
|
<input type="hidden" name="addr_search_sigungu" id="addr_search_sigungu" value="<?= esc((string) old('addr_search_sigungu', '')) ?>"/>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소번호</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">판매소번호</label>
|
||||||
<div class="text-sm text-gray-600">등록 시 자동 부여 (지자체코드 + 일련번호 3자리)</div>
|
<div class="text-sm text-gray-600">등록 시 자동 부여 (주소 기준 기본코드 B·C·D 조합 + 일련번호 3자리)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -49,23 +53,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">업태</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_number" type="text" value="<?= esc(old('ds_va_number')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_type" type="text" value="<?= esc(old('ds_biz_type')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">업종</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_kind" type="text" value="<?= esc(old('ds_biz_kind')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌(은행)</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_va_bank" type="text" value="<?= esc(old('ds_va_bank')) ?>" placeholder="예: 농협"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">계좌번호</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_account" type="text" value="<?= esc(old('ds_va_account')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-start gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">주소</label>
|
||||||
|
<p class="text-xs text-gray-600 max-w-lg leading-relaxed">우편번호 옆 <strong>주소 검색</strong>으로만 지정합니다(직접 입력 불가). <strong>지도</strong>는 카카오맵에서 현재 입력된 주소를 검색해 엽니다. 관할이 아니면 검색 직후 안내 후 반영되지 않습니다. 동·호 등은 아래 <strong>상세주소</strong>에 입력하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="ds_zip" type="text" value="<?= esc(old('ds_zip')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_zip" id="ds_zip" type="text" value="<?= esc(old('ds_zip')) ?>" maxlength="10" autocomplete="postal-code" readonly tabindex="-1"/>
|
||||||
|
<button type="button" id="btn-ds-kakao-postcode" class="no-print border border-btn-print-border text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition shrink-0">주소 검색</button>
|
||||||
|
<?= view('components/kakao_map_link_button', ['buttonId' => 'btn-ds-kakao-map-create']) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr" type="text" value="<?= esc(old('ds_addr')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr" id="ds_addr" type="text" value="<?= esc(old('ds_addr')) ?>" readonly tabindex="-1"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr_jibun" type="text" value="<?= esc(old('ds_addr_jibun')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr_jibun" id="ds_addr_jibun" type="text" value="<?= esc(old('ds_addr_jibun')) ?>" readonly tabindex="-1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">상세주소</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full" name="ds_addr_detail" id="ds_addr_detail" type="text" value="<?= esc(old('ds_addr_detail')) ?>" maxlength="200" placeholder="동·호수·층 등"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -88,11 +119,31 @@
|
|||||||
<div class="text-sm text-gray-600">해당 지자체(구·군) 코드로 등록 시 자동 설정</div>
|
<div class="text-sm text-gray-600">해당 지자체(구·군) 코드로 등록 시 자동 설정</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">구역</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" name="ds_zone_code" type="text" value="<?= esc(old('ds_zone_code')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">종사업장번호</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_branch_no" type="text" value="<?= esc(old('ds_branch_no')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc(old('ds_designated_at')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc(old('ds_designated_at')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">변경일자</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_state_changed_at" type="date" value="<?= esc(old('ds_state_changed_at')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-start gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">변경사유</label>
|
||||||
|
<textarea class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 min-h-[4rem]" name="ds_change_reason" rows="3"><?= esc(old('ds_change_reason')) ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||||
<a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
<a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
||||||
@@ -100,3 +151,17 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
|
||||||
|
|
||||||
|
<?= view('components/kakao_address_search', [
|
||||||
|
'buttonId' => 'btn-ds-kakao-postcode',
|
||||||
|
'zipName' => 'ds_zip',
|
||||||
|
'roadName' => 'ds_addr',
|
||||||
|
'jibunName' => 'ds_addr_jibun',
|
||||||
|
'sidoFieldName' => 'addr_search_sido',
|
||||||
|
'sigunguFieldName' => 'addr_search_sigungu',
|
||||||
|
'tenantScope' => $addrTenantScope ?? ['lg_sido' => '', 'lg_gugun' => ''],
|
||||||
|
'roadBaseOnly' => true,
|
||||||
|
'detailFieldName' => 'ds_addr_detail',
|
||||||
|
]) ?>
|
||||||
|
|
||||||
|
|||||||
210
app/Views/admin/designated_shop/district_new_cancel.php
Normal file
210
app/Views/admin/designated_shop/district_new_cancel.php
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
$ry = (int) ($reportYear ?? (int) date('Y'));
|
||||||
|
$lg = $currentLg ?? null;
|
||||||
|
$lgSido = $lg !== null ? trim((string) ($lg->lg_sido ?? '')) : '';
|
||||||
|
$lgGugun = $lg !== null ? trim((string) ($lg->lg_gugun ?? '')) : '';
|
||||||
|
$lgName = $lg !== null ? trim((string) ($lg->lg_name ?? '')) : '';
|
||||||
|
$scopeLabel = $lgSido !== '' && $lgGugun !== ''
|
||||||
|
? $lgSido . ' ' . $lgGugun
|
||||||
|
: ($lgName !== '' ? $lgName : '—');
|
||||||
|
$exportUrl = mgmt_url('designated-shops/district-new-cancel/export') . '?' . http_build_query(['year' => $ry]);
|
||||||
|
?>
|
||||||
|
<?= view('components/print_header', ['printTitle' => '지정 판매소 신규/취소 현황 (' . $ry . '년)']) ?>
|
||||||
|
<style>
|
||||||
|
.gbms-dnc-wrap { max-width: 100%; }
|
||||||
|
.gbms-dnc-table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
||||||
|
.gbms-dnc-table th,
|
||||||
|
.gbms-dnc-table td {
|
||||||
|
border: 1px solid #7a8aa0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.gbms-dnc-table thead th {
|
||||||
|
background: linear-gradient(180deg, #e8eef6 0%, #d4dee9 100%);
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a2a3a;
|
||||||
|
}
|
||||||
|
.gbms-dnc-table thead th.gbms-sub {
|
||||||
|
background: #dce6f0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.gbms-dnc-table tbody td.text-left { text-align: left; }
|
||||||
|
.gbms-dnc-table tbody tr.gbms-total td {
|
||||||
|
font-weight: 700;
|
||||||
|
border: 2px solid #c62828;
|
||||||
|
background: #fff8f8;
|
||||||
|
}
|
||||||
|
.gbms-dnc-caption {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 8px 0 6px;
|
||||||
|
color: #1a2a3a;
|
||||||
|
}
|
||||||
|
.gbms-unit-pill {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0d47a1;
|
||||||
|
background: #e3f2fd;
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.gbms-tip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.gbms-help {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
padding: 0 2px;
|
||||||
|
border: 1px solid #5c6f85;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fef9c3;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
user-select: none;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
.gbms-help::after {
|
||||||
|
content: attr(data-tip);
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: none;
|
||||||
|
min-width: 12rem;
|
||||||
|
max-width: 14rem;
|
||||||
|
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: 30;
|
||||||
|
}
|
||||||
|
.gbms-help:hover::after,
|
||||||
|
.gbms-help:focus::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.gbms-dnc-table { font-size: 11px; }
|
||||||
|
.gbms-help { display: none !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<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-800">[지정 판매소 신규/취소 현황]</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-3 bg-white border-b border-gray-200 no-print">
|
||||||
|
<form method="get" action="<?= mgmt_url('designated-shops/district-new-cancel') ?>" class="flex flex-wrap items-end gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-600 mb-0.5">조회년도</label>
|
||||||
|
<select name="year" class="border border-gray-400 rounded px-2 py-1.5 text-sm min-w-[7rem] bg-white">
|
||||||
|
<?php foreach (($yearChoices ?? []) as $y): ?>
|
||||||
|
<option value="<?= (int) $y ?>" <?= $ry === (int) $y ? 'selected' : '' ?>><?= (int) $y ?>년</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-[12rem]">
|
||||||
|
<span class="block text-xs text-gray-600 mb-0.5">군·구 (소속 지자체)</span>
|
||||||
|
<div class="border border-gray-300 rounded px-3 py-1.5 text-sm bg-gray-50 text-gray-800 font-medium">
|
||||||
|
<?= esc($scopeLabel) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="gbms-unit-pill self-end mb-0.5">단위: 판매소</span>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm font-medium shadow-sm hover:opacity-90">조회</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-3 mb-4 gbms-dnc-wrap">
|
||||||
|
<div class="gbms-dnc-caption">지정 판매소 신규/취소 현황 조회 내역</div>
|
||||||
|
<div class="overflow-x-auto border border-gray-400 bg-white">
|
||||||
|
<table class="gbms-dnc-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2" class="min-w-[6rem]">군·구</th>
|
||||||
|
<th rowspan="2">
|
||||||
|
<span class="gbms-tip">
|
||||||
|
종전
|
||||||
|
<span class="gbms-help" tabindex="0" aria-label="종전 설명" data-tip="전년도 12월 31일 기준 정상 상태 판매소 수">?</span>
|
||||||
|
</span>
|
||||||
|
<br><span class="text-xs font-normal">(전년도말)</span>
|
||||||
|
</th>
|
||||||
|
<th colspan="2">사용</th>
|
||||||
|
<th rowspan="2">
|
||||||
|
<span class="gbms-tip">
|
||||||
|
현행
|
||||||
|
<span class="gbms-help" tabindex="0" aria-label="현행 설명" data-tip="조회년도 12월 31일 기준 정상 상태 판매소 수">?</span>
|
||||||
|
</span>
|
||||||
|
<br><span class="text-xs font-normal">(금년도말)</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="gbms-sub">
|
||||||
|
<span class="gbms-tip">
|
||||||
|
지정
|
||||||
|
<span class="gbms-help" tabindex="0" aria-label="지정 설명" data-tip="조회년도 내 지정일이 속한 신규 지정 건수">?</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th class="gbms-sub">
|
||||||
|
<span class="gbms-tip">
|
||||||
|
취소
|
||||||
|
<span class="gbms-help" tabindex="0" aria-label="취소 설명" data-tip="조회년도 내 폐업/해지 전환일이 속한 건수">?</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach (($districtRows ?? []) as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-left font-medium"><?= esc($row->region_label) ?></td>
|
||||||
|
<td><?= number_format((int) $row->prev_end) ?></td>
|
||||||
|
<td><?= number_format((int) $row->designated_y) ?></td>
|
||||||
|
<td><?= number_format((int) $row->cancelled_y) ?></td>
|
||||||
|
<td><?= number_format((int) $row->curr_end) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($districtRows)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-gray-500 py-8">표시할 구·군 또는 지정판매소 데이터가 없습니다.</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (! empty($districtRows) && isset($districtTotal)): ?>
|
||||||
|
<tr class="gbms-total">
|
||||||
|
<td class="text-left"><?= esc($districtTotal->region_label) ?></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>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -5,20 +5,35 @@ if ($shop === null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($key) : ($shop->{$key} ?? $default);
|
$v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($key) : ($shop->{$key} ?? $default);
|
||||||
|
$vaAccountDefault = (isset($shop->ds_va_account) && (string) $shop->ds_va_account !== '')
|
||||||
|
? (string) $shop->ds_va_account
|
||||||
|
: (string) ($shop->ds_va_number ?? '');
|
||||||
|
$dateField = static function (string $key) use ($shop, $v): string {
|
||||||
|
$s = (string) $v($key);
|
||||||
|
if ($s === '' || str_starts_with($s, '0000')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $s;
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<span class="text-sm font-bold text-gray-700">지정판매소 수정</span>
|
<span class="text-sm font-bold text-gray-700">지정판매소 수정</span>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
<form action="<?= mgmt_url('designated-shops/update/' . (int) $shop->ds_idx) ?>" method="POST" class="space-y-4">
|
<form id="designated-shop-edit-form" action="<?= mgmt_url('designated-shops/update/' . (int) $shop->ds_idx) ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<input type="hidden" name="addr_search_sido" id="addr_search_sido" value="<?= esc((string) old('addr_search_sido', $currentLg !== null ? (string) ($currentLg->lg_sido ?? '') : '')) ?>"/>
|
||||||
|
<input type="hidden" name="addr_search_sigungu" id="addr_search_sigungu" value="<?= esc((string) old('addr_search_sigungu', $currentLg !== null ? (string) ($currentLg->lg_gugun ?? '') : '')) ?>"/>
|
||||||
|
|
||||||
<?php if ($currentLg !== null): ?>
|
<?php if ($currentLg !== null): ?>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지자체</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">지자체</label>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
||||||
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
||||||
|
<span class="text-gray-500 ml-1"><?= esc(trim((string) ($currentLg->lg_sido ?? '') . ' ' . (string) ($currentLg->lg_gugun ?? ''))) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -49,23 +64,50 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">업태</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_number" type="text" value="<?= esc($v('ds_va_number')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_type" type="text" value="<?= esc($v('ds_biz_type')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">업종</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_biz_kind" type="text" value="<?= esc($v('ds_biz_kind')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">가상계좌(은행)</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_va_bank" type="text" value="<?= esc($v('ds_va_bank')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">계좌번호</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_account" type="text" value="<?= esc((old('ds_va_account') !== null && old('ds_va_account') !== '') ? old('ds_va_account') : $vaAccountDefault) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-start gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">주소</label>
|
||||||
|
<p class="text-xs text-gray-600 max-w-lg leading-relaxed">우편·도로명·지번은 <strong>주소 검색</strong>으로만 바꿀 수 있습니다(직접 입력 불가). <strong>지도</strong>는 카카오맵에서 현재 입력된 주소를 검색해 엽니다. 관할이 아니면 검색 직후 안내 후 반영되지 않습니다. 동·호 등은 <strong>상세주소</strong>에 입력하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="ds_zip" type="text" value="<?= esc($v('ds_zip')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_zip" id="ds_zip" type="text" value="<?= esc($v('ds_zip')) ?>" maxlength="10" readonly tabindex="-1"/>
|
||||||
|
<button type="button" id="btn-ds-kakao-postcode-edit" class="no-print border border-btn-print-border text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition shrink-0">주소 검색</button>
|
||||||
|
<?= view('components/kakao_map_link_button', ['buttonId' => 'btn-ds-kakao-map-edit']) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr" type="text" value="<?= esc($v('ds_addr')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr" id="ds_addr" type="text" value="<?= esc($v('ds_addr')) ?>" readonly tabindex="-1"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr_jibun" type="text" value="<?= esc($v('ds_addr_jibun')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr_jibun" id="ds_addr_jibun" type="text" value="<?= esc($v('ds_addr_jibun')) ?>" readonly tabindex="-1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">상세주소</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 max-w-full" name="ds_addr_detail" id="ds_addr_detail" type="text" value="<?= esc($v('ds_addr_detail')) ?>" maxlength="200" placeholder="동·호수·층 등"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -83,6 +125,16 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
|
|||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_email" type="email" value="<?= esc($v('ds_email')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_email" type="email" value="<?= esc($v('ds_email')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">구역</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" name="ds_zone_code" type="text" value="<?= esc($v('ds_zone_code')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">종사업장번호</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-48" name="ds_branch_no" type="text" value="<?= esc($v('ds_branch_no')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc($v('ds_designated_at')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc($v('ds_designated_at')) ?>"/>
|
||||||
@@ -97,9 +149,33 @@ $v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($k
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">변경일자</label>
|
||||||
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_state_changed_at" type="date" value="<?= esc($dateField('ds_state_changed_at')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-start gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">변경사유</label>
|
||||||
|
<textarea class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 min-h-[4rem]" name="ds_change_reason" rows="3"><?= esc($v('ds_change_reason')) ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
|
||||||
<a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
<a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
|
||||||
|
|
||||||
|
<?= view('components/kakao_address_search', [
|
||||||
|
'buttonId' => 'btn-ds-kakao-postcode-edit',
|
||||||
|
'zipName' => 'ds_zip',
|
||||||
|
'roadName' => 'ds_addr',
|
||||||
|
'jibunName' => 'ds_addr_jibun',
|
||||||
|
'sidoFieldName' => 'addr_search_sido',
|
||||||
|
'sigunguFieldName' => 'addr_search_sigungu',
|
||||||
|
'tenantScope' => $addrTenantScope ?? ['lg_sido' => '', 'lg_gugun' => ''],
|
||||||
|
'roadBaseOnly' => true,
|
||||||
|
'detailFieldName' => 'ds_addr_detail',
|
||||||
|
]) ?>
|
||||||
|
|||||||
@@ -1,24 +1,202 @@
|
|||||||
<?= view('components/print_header', ['printTitle' => '지정판매소 목록']) ?>
|
<?php
|
||||||
|
helper('admin');
|
||||||
|
$currentPath = current_nav_request_path();
|
||||||
|
if ($currentPath === 'bag/designated-shops') {
|
||||||
|
$readOnly = false;
|
||||||
|
} elseif ($currentPath === 'bag/designated-shops/browse') {
|
||||||
|
$readOnly = true;
|
||||||
|
} else {
|
||||||
|
$readOnly = ! empty($readOnly);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?= view('components/print_header', ['printTitle' => $readOnly ? '지정판매소 조회 목록' : '지정판매소 목록']) ?>
|
||||||
|
<style>
|
||||||
|
/* 목록 위 → 지정판매소 정보 아래 (가로 2열 없음) */
|
||||||
|
.ds-split {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.ds-list-panel {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 42vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.ds-detail-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 12rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.ds-panel-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: linear-gradient(180deg, #fafafa 0%, #e9ecef 100%);
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.ds-summary-bar {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.ds-row-selected td { background-color: #cce5ff !important; }
|
||||||
|
.ds-detail-inner { padding: 10px; overflow: auto; flex: 1; }
|
||||||
|
/* 원본 지정판매소 정보: 라벨 고정폭 + 2열 값(우측 값이 더 넓음), 주소·개인전화는 전폭 */
|
||||||
|
.ds-detail-form {
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.ds-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.ds-detail-form > .ds-row:last-child { border-bottom: none; }
|
||||||
|
/* 그 외 2+2 동일 비율 (상호명 | 우편번호 등) */
|
||||||
|
.ds-row-4-even {
|
||||||
|
grid-template-columns: 5.5rem minmax(0, 1fr) 5.5rem minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
/* 판매소번호 전폭 행 — 값을 우편·주소 필드처럼 넓게 */
|
||||||
|
.ds-value-shop-wide {
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-size: 13px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
/* 라벨 | 값(나머지 전체) — 도로명·지번·개인전화·이메일 */
|
||||||
|
.ds-row-wide {
|
||||||
|
grid-template-columns: 5.5rem minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.ds-row-wide-tall .ds-field-value {
|
||||||
|
min-height: 3.25rem;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
/* 도로명주소 + 카카오맵 버튼 */
|
||||||
|
.ds-field-value-with-map {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ds-field-value-with-map .ds-addr-text {
|
||||||
|
flex: 1 1 12rem;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.ds-field-label {
|
||||||
|
background: #eef2f5;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.ds-field-value {
|
||||||
|
padding: 5px 8px;
|
||||||
|
background: #fff;
|
||||||
|
word-break: break-word;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.ds-row-4-even > *:nth-child(4n) { border-right: none; }
|
||||||
|
.ds-row-wide > .ds-field-value { border-right: none; }
|
||||||
|
.ds-field-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #b91c1c;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.ds-row-4-even { grid-template-columns: 5rem 1fr; }
|
||||||
|
}
|
||||||
|
.ds-detail-actions { padding: 10px; border-top: 1px solid #ccc; background: #eee; }
|
||||||
|
.ds-detail-info-wrap { overflow-x: auto; }
|
||||||
|
.ds-detail-info-wrap .data-table th { white-space: nowrap; }
|
||||||
|
.ds-detail-info-wrap th.ds-col-tight-head,
|
||||||
|
.ds-detail-info-wrap td.ds-col-tight-cell {
|
||||||
|
max-width: 6.5rem;
|
||||||
|
width: 6.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ds-list-panel .ds-col-tight {
|
||||||
|
max-width: 6rem;
|
||||||
|
width: 6rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ds-list-panel .ds-col-zip {
|
||||||
|
width: 4.5rem;
|
||||||
|
max-width: 5rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ds-list-panel .ds-col-addr-list {
|
||||||
|
max-width: 11rem;
|
||||||
|
min-width: 6rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ds-list-panel .ds-col-detail-list {
|
||||||
|
max-width: 8rem;
|
||||||
|
min-width: 4rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ds-ro-road-btn { margin-left: 6px; vertical-align: middle; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$listBasePath = $readOnly ? 'designated-shops/browse' : 'designated-shops';
|
||||||
|
?>
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">지정판매소 목록</span>
|
<span class="text-sm font-bold text-gray-700"><?= $readOnly ? '지정판매소 조회' : '지정판매소 관리' ?></span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<?php if ($readOnly): ?>
|
||||||
<a href="<?= mgmt_url('designated-shops/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
<a href="<?= mgmt_url('designated-shops/export') ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||||
<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>
|
<button type="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>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (! $readOnly): ?>
|
||||||
<a href="<?= mgmt_url('designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a>
|
<a href="<?= mgmt_url('designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- P2-15: 다조건 검색 -->
|
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
<form method="GET" action="<?= mgmt_url('designated-shops') ?>" class="flex flex-wrap items-center gap-2">
|
<form method="GET" action="<?= mgmt_url($listBasePath) ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="text-sm font-semibold text-gray-700 mr-1">지정판매소 검색</span>
|
||||||
<label class="text-sm text-gray-600">상호명</label>
|
<label class="text-sm text-gray-600">상호명</label>
|
||||||
<input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명 검색" class="border border-gray-300 rounded px-2 py-1 text-sm w-40"/>
|
<input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명" class="border border-gray-300 rounded px-2 py-1 text-sm w-36"/>
|
||||||
<label class="text-sm text-gray-600">구군코드</label>
|
<label class="text-sm text-gray-600">구·군 코드</label>
|
||||||
<select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
<select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[14rem]">
|
||||||
<option value="">전체</option>
|
<option value="">전체</option>
|
||||||
<?php foreach (($gugunCodes ?? []) as $gc): ?>
|
<?php foreach (($gugunCodes ?? []) as $gc): ?>
|
||||||
<option value="<?= esc($gc->ds_gugun_code) ?>" <?= ($dsGugunCode ?? '') === $gc->ds_gugun_code ? 'selected' : '' ?>><?= esc($gc->ds_gugun_code) ?></option>
|
<?php $gCode = (string) ($gc->ds_gugun_code ?? ''); ?>
|
||||||
|
<option value="<?= esc($gCode) ?>" <?= ($dsGugunCode ?? '') === $gCode ? 'selected' : '' ?>><?= esc((string) (($gugunNameMap[$gCode] ?? '') !== '' ? $gugunNameMap[$gCode] : $gCode)) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<label class="text-sm text-gray-600">상태</label>
|
<label class="text-sm text-gray-600">상태</label>
|
||||||
@@ -29,48 +207,357 @@
|
|||||||
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
|
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</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>
|
<a href="<?= mgmt_url($listBasePath) ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
|
||||||
<table class="w-full data-table">
|
<?php
|
||||||
|
$sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0];
|
||||||
|
?>
|
||||||
|
<div class="ds-summary-bar no-print mx-2 mt-2 rounded-sm">
|
||||||
|
건수 : <?= (int) ($sc['total'] ?? 0) ?>
|
||||||
|
(정상 : <?= (int) ($sc[1] ?? 0) ?> / 폐업 : <?= (int) ($sc[2] ?? 0) ?> / 해지 : <?= (int) ($sc[3] ?? 0) ?>)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ds-split no-print mx-2 mb-2 mt-2 flex-1 min-h-0">
|
||||||
|
<div class="ds-list-panel">
|
||||||
|
<div class="ds-panel-title shrink-0">지정판매소 리스트</div>
|
||||||
|
<div class="overflow-auto flex-1 min-h-0">
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-14">번호</th>
|
||||||
|
<th class="w-24">구·군</th>
|
||||||
|
<th class="w-24">지정일</th>
|
||||||
|
<th class="w-24">구역</th>
|
||||||
|
<th class="ds-col-tight">대표자명</th>
|
||||||
|
<th class="ds-col-tight">상호명</th>
|
||||||
|
<th class="ds-col-zip">우편번호</th>
|
||||||
|
<th class="text-left">주소</th>
|
||||||
|
<th class="w-28">사업자번호</th>
|
||||||
|
<th class="w-28">전화</th>
|
||||||
|
<th class="w-16">상태</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="ds-list-body" class="text-right">
|
||||||
|
<?php foreach ($list as $i => $row): ?>
|
||||||
|
<?php
|
||||||
|
$sn = (string) ($row->ds_shop_no ?? '');
|
||||||
|
if (preg_match('/(\d{3})$/', $sn, $m)) {
|
||||||
|
$shortNo = $m[1];
|
||||||
|
} elseif ($sn !== '' && strlen($sn) >= 3) {
|
||||||
|
$shortNo = substr($sn, -3);
|
||||||
|
} else {
|
||||||
|
$shortNo = $sn;
|
||||||
|
}
|
||||||
|
$st = (int) ($row->ds_state ?? 1);
|
||||||
|
$stLabel = $st === 1 ? '' : ($st === 2 ? '폐업' : '해지');
|
||||||
|
$ggCode = (string) ($row->ds_gugun_code ?? '');
|
||||||
|
$ggLabel = (string) (($gugunNameMap[$ggCode] ?? '') !== '' ? $gugunNameMap[$ggCode] : $ggCode);
|
||||||
|
$da = $row->ds_designated_at ?? null;
|
||||||
|
$daDisp = ($da !== null && $da !== '' && (string) $da !== '0000-00-00') ? substr((string) $da, 0, 10) : '';
|
||||||
|
$zone = (string) ($row->ds_zone_code ?? '');
|
||||||
|
$zipList = trim((string) ($row->ds_zip ?? ''));
|
||||||
|
$roadL = trim((string) ($row->ds_addr ?? ''));
|
||||||
|
$jibunL = trim((string) ($row->ds_addr_jibun ?? ''));
|
||||||
|
$addrMainList = $roadL !== '' ? $roadL : $jibunL;
|
||||||
|
$addrDetailList = trim((string) ($row->ds_addr_detail ?? ''));
|
||||||
|
$addrCombinedList = trim($addrMainList . ' ' . $addrDetailList);
|
||||||
|
if ($addrCombinedList === '') {
|
||||||
|
$addrCombinedList = $addrMainList;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<tr class="ds-list-row cursor-pointer" data-row-index="<?= (int) $i ?>" role="button" tabindex="0">
|
||||||
|
<td class="text-center"><?= esc($shortNo) ?></td>
|
||||||
|
<td class="text-left pl-1 text-xs"><?= esc($ggLabel) ?></td>
|
||||||
|
<td class="text-center text-xs"><?= esc($daDisp) ?></td>
|
||||||
|
<td class="text-left pl-1 text-xs"><?= esc($zone) ?></td>
|
||||||
|
<td class="text-left pl-1 text-xs ds-col-tight" title="<?= esc($row->ds_rep_name ?? '') ?>"><?= esc($row->ds_rep_name ?? '') ?></td>
|
||||||
|
<td class="text-left pl-1 text-xs ds-col-tight" title="<?= esc($row->ds_name ?? '') ?>"><?= esc($row->ds_name ?? '') ?></td>
|
||||||
|
<td class="text-center text-xs ds-col-zip" title="<?= esc($zipList) ?>"><?= esc($zipList) ?></td>
|
||||||
|
<td class="text-left pl-1 text-xs ds-col-addr-list" title="<?= esc($addrCombinedList) ?>"><?= esc($addrCombinedList) ?></td>
|
||||||
|
<td class="text-left pl-1 text-xs"><?= esc($row->ds_biz_no ?? '') ?></td>
|
||||||
|
<td class="text-left pl-1 text-xs"><?= esc($row->ds_tel ?? '') ?></td>
|
||||||
|
<td class="text-center <?= $st === 2 ? 'text-pink-600 font-medium' : ($st === 3 ? 'text-orange-700' : '') ?>"><?= esc($stLabel) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ds-detail-panel">
|
||||||
|
<div class="ds-panel-title shrink-0">지정판매소 정보</div>
|
||||||
|
<div class="ds-detail-inner" id="ds-detail-box">
|
||||||
|
<p id="ds-detail-placeholder" class="text-sm text-gray-500 py-6 text-center">위 목록에서 행을 선택하세요.</p>
|
||||||
|
<div id="ds-detail-fields" class="hidden">
|
||||||
|
<div class="ds-detail-info-wrap">
|
||||||
|
<table class="w-full data-table text-sm" id="ds-detail-info-table" aria-label="지정판매소 상세">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>판매소번호</th>
|
||||||
|
<th class="ds-col-tight-head">상호명</th>
|
||||||
|
<th>우편번호</th>
|
||||||
|
<th>사업자번호</th>
|
||||||
|
<th>일반전화</th>
|
||||||
|
<th class="ds-col-tight-head">대표자명</th>
|
||||||
|
<th>이메일</th>
|
||||||
|
<th>업태</th>
|
||||||
|
<th>업종</th>
|
||||||
|
<th>지정일자</th>
|
||||||
|
<th>지자체</th>
|
||||||
|
<th>도로명주소</th>
|
||||||
|
<th>지번주소</th>
|
||||||
|
<th>상세주소</th>
|
||||||
|
<th>개인전화</th>
|
||||||
|
<th>구·군</th>
|
||||||
|
<th>구역</th>
|
||||||
|
<th>가상계좌(은행)</th>
|
||||||
|
<th>계좌번호</th>
|
||||||
|
<th>종사업장번호</th>
|
||||||
|
<th>변경일자</th>
|
||||||
|
<th>영업상태</th>
|
||||||
|
<th>등록일시</th>
|
||||||
|
<th>변경사유</th>
|
||||||
|
<th class="no-print w-14">지도</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-left" data-ro="ds_shop_no">—</td>
|
||||||
|
<td class="text-left ds-col-tight-cell" data-ro="ds_name">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_zip">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_biz_no">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_tel">—</td>
|
||||||
|
<td class="text-left ds-col-tight-cell" data-ro="ds_rep_name">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_email">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_biz_type">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_biz_kind">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_designated_at">—</td>
|
||||||
|
<td class="text-left" data-ro="lg_name">—</td>
|
||||||
|
<td class="text-left min-w-[10rem]"><span data-ro="ds_addr">—</span></td>
|
||||||
|
<td class="text-left" data-ro="ds_addr_jibun">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_addr_detail">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_rep_phone">—</td>
|
||||||
|
<td class="text-left" data-ro="gugun_name">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_zone_code">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_va_bank">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_va_account">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_branch_no">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_state_changed_at">—</td>
|
||||||
|
<td class="text-left" data-ro="state_label">—</td>
|
||||||
|
<td class="text-left" data-ro="ds_regdate">—</td>
|
||||||
|
<td class="text-left min-w-[8rem]" data-ro="ds_change_reason">—</td>
|
||||||
|
<td class="text-center no-print">
|
||||||
|
<button type="button" class="border border-btn-print-border text-gray-700 px-2 py-0.5 rounded-sm text-xs hover:bg-gray-50" id="ds-ro-map-btn">지도</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if (! $readOnly): ?>
|
||||||
|
<div class="ds-detail-actions no-print flex flex-wrap items-center gap-3 shrink-0">
|
||||||
|
<a id="ds-edit-link" href="#" class="text-blue-700 hover:underline text-sm font-medium pointer-events-none opacity-40">수정</a>
|
||||||
|
<form id="ds-delete-form" method="POST" action="" class="inline" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit" id="ds-delete-btn" class="text-red-600 hover:underline text-sm pointer-events-none opacity-40" disabled>삭제</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($pager)): ?>
|
||||||
|
<div class="mt-2 mb-2 mx-2 no-print"><?= $pager->links() ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
|
||||||
|
|
||||||
|
<script type="application/json" id="ds-detail-json"><?= $detailRowsJson ?? '[]' ?></script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var raw = document.getElementById('ds-detail-json');
|
||||||
|
var rows = [];
|
||||||
|
try {
|
||||||
|
rows = JSON.parse(raw.textContent || '[]');
|
||||||
|
} catch (e) {
|
||||||
|
rows = [];
|
||||||
|
}
|
||||||
|
var readOnly = <?= json_encode($readOnly) ?>;
|
||||||
|
var body = document.getElementById('ds-list-body');
|
||||||
|
var placeholder = document.getElementById('ds-detail-placeholder');
|
||||||
|
var fieldsWrap = document.getElementById('ds-detail-fields');
|
||||||
|
var infoTable = document.getElementById('ds-detail-info-table');
|
||||||
|
var editLink = readOnly ? null : document.getElementById('ds-edit-link');
|
||||||
|
var delForm = readOnly ? null : document.getElementById('ds-delete-form');
|
||||||
|
var delBtn = readOnly ? null : document.getElementById('ds-delete-btn');
|
||||||
|
// mgmt_url() 이 path 를 trim 하므로 'edit/33' 이 아니라 'edit33' 로 붙지 않게 슬래시를 넣음
|
||||||
|
var editBase = <?= json_encode(mgmt_url('designated-shops/edit')) ?>;
|
||||||
|
var delBase = <?= json_encode(mgmt_url('designated-shops/delete')) ?>;
|
||||||
|
|
||||||
|
function textVal(v) {
|
||||||
|
return (v === '' || v == null) ? '—' : String(v);
|
||||||
|
}
|
||||||
|
function buildKakaoMapSearchQuery(d) {
|
||||||
|
var road = String(d.ds_addr || '').trim();
|
||||||
|
var jibun = String(d.ds_addr_jibun || '').trim();
|
||||||
|
var detail = String(d.ds_addr_detail || '').trim();
|
||||||
|
var q = road || jibun;
|
||||||
|
if (detail) {
|
||||||
|
q = q ? (q + ' ' + detail) : detail;
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
function fillDetailInfoTable(d) {
|
||||||
|
if (!infoTable) return;
|
||||||
|
infoTable.querySelectorAll('[data-ro]').forEach(function (el) {
|
||||||
|
var k = el.getAttribute('data-ro');
|
||||||
|
var v = d[k];
|
||||||
|
if (k === 'ds_va_account') {
|
||||||
|
v = d.ds_va_account || d.ds_va_number || '';
|
||||||
|
}
|
||||||
|
el.textContent = textVal(v);
|
||||||
|
});
|
||||||
|
window.__dsDetailForMap = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIndex(idx) {
|
||||||
|
if (!rows.length || idx < 0 || idx >= rows.length) return;
|
||||||
|
var d = rows[idx];
|
||||||
|
Array.prototype.forEach.call(body.querySelectorAll('tr.ds-list-row'), function (tr) {
|
||||||
|
tr.classList.remove('ds-row-selected');
|
||||||
|
});
|
||||||
|
var tr = body.querySelector('tr[data-row-index="' + idx + '"]');
|
||||||
|
if (tr) tr.classList.add('ds-row-selected');
|
||||||
|
|
||||||
|
placeholder.classList.add('hidden');
|
||||||
|
fieldsWrap.classList.remove('hidden');
|
||||||
|
fillDetailInfoTable(d);
|
||||||
|
|
||||||
|
if (!readOnly && editLink && delForm && delBtn) {
|
||||||
|
var id = d.ds_idx;
|
||||||
|
editLink.href = editBase + '/' + id;
|
||||||
|
editLink.classList.remove('pointer-events-none', 'opacity-40');
|
||||||
|
delForm.action = delBase + '/' + id;
|
||||||
|
delBtn.disabled = false;
|
||||||
|
delBtn.classList.remove('pointer-events-none', 'opacity-40');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
body.addEventListener('click', function (e) {
|
||||||
|
var tr = e.target.closest('tr.ds-list-row');
|
||||||
|
if (!tr) return;
|
||||||
|
var idx = parseInt(tr.getAttribute('data-row-index'), 10);
|
||||||
|
if (!isNaN(idx)) selectIndex(idx);
|
||||||
|
});
|
||||||
|
body.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
var tr = e.target.closest('tr.ds-list-row');
|
||||||
|
if (!tr) return;
|
||||||
|
e.preventDefault();
|
||||||
|
var idx = parseInt(tr.getAttribute('data-row-index'), 10);
|
||||||
|
if (!isNaN(idx)) selectIndex(idx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapBtnRo = document.getElementById('ds-ro-map-btn');
|
||||||
|
if (mapBtnRo) {
|
||||||
|
mapBtnRo.addEventListener('click', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var d = window.__dsDetailForMap;
|
||||||
|
if (!d) return;
|
||||||
|
var q = buildKakaoMapSearchQuery(d);
|
||||||
|
if (!q) {
|
||||||
|
window.alert('표시할 주소 정보가 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window.openDesignatedShopKakaoMap === 'function') {
|
||||||
|
window.openDesignatedShopKakaoMap(q);
|
||||||
|
} else {
|
||||||
|
window.open('https://map.kakao.com/link/search/' + encodeURIComponent(q), '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
selectIndex(0);
|
||||||
|
} else if (!readOnly && editLink && delBtn) {
|
||||||
|
editLink.classList.add('pointer-events-none', 'opacity-40');
|
||||||
|
delBtn.disabled = true;
|
||||||
|
delBtn.classList.add('pointer-events-none', 'opacity-40');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 인쇄용: 전체 테이블 -->
|
||||||
|
<div class="hidden print:block print:p-4">
|
||||||
|
<table class="w-full data-table text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16">번호</th>
|
<th>번호</th>
|
||||||
<th>지자체</th>
|
<th>지자체</th>
|
||||||
<th>판매소번호</th>
|
<th>구·군</th>
|
||||||
|
<th>지정일</th>
|
||||||
|
<th>구역</th>
|
||||||
|
<th>대표자명</th>
|
||||||
<th>상호명</th>
|
<th>상호명</th>
|
||||||
<th>대표자</th>
|
<th>우편번호</th>
|
||||||
|
<th>주소</th>
|
||||||
<th>사업자번호</th>
|
<th>사업자번호</th>
|
||||||
|
<th>전화</th>
|
||||||
|
<th>판매소번호</th>
|
||||||
<th>가상계좌</th>
|
<th>가상계좌</th>
|
||||||
<th>상태</th>
|
<th>상태</th>
|
||||||
<th>등록일</th>
|
<th>등록일</th>
|
||||||
<th class="w-28">작업</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody>
|
||||||
<?php foreach ($list as $row): ?>
|
<?php foreach ($list as $row): ?>
|
||||||
|
<?php
|
||||||
|
$snP = (string) ($row->ds_shop_no ?? '');
|
||||||
|
if (preg_match('/(\d{3})$/', $snP, $mP)) {
|
||||||
|
$shortNoP = $mP[1];
|
||||||
|
} elseif ($snP !== '' && strlen($snP) >= 3) {
|
||||||
|
$shortNoP = substr($snP, -3);
|
||||||
|
} else {
|
||||||
|
$shortNoP = $snP;
|
||||||
|
}
|
||||||
|
$daP = $row->ds_designated_at ?? null;
|
||||||
|
$daDispP = ($daP !== null && $daP !== '' && (string) $daP !== '0000-00-00') ? substr((string) $daP, 0, 10) : '';
|
||||||
|
$stP = (int) ($row->ds_state ?? 1);
|
||||||
|
$stLabP = $stP === 1 ? '정상' : ($stP === 2 ? '폐업' : '직권해지');
|
||||||
|
$zipP = trim((string) ($row->ds_zip ?? ''));
|
||||||
|
$roadP = trim((string) ($row->ds_addr ?? ''));
|
||||||
|
$jibP = trim((string) ($row->ds_addr_jibun ?? ''));
|
||||||
|
$addrP = $roadP !== '' ? $roadP : $jibP;
|
||||||
|
$detP = trim((string) ($row->ds_addr_detail ?? ''));
|
||||||
|
$addrCombinedP = trim($addrP . ' ' . $detP);
|
||||||
|
if ($addrCombinedP === '') {
|
||||||
|
$addrCombinedP = $addrP;
|
||||||
|
}
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->ds_idx) ?></td>
|
<td class="text-center"><?= esc($shortNoP) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
|
<td class="text-left"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_shop_no) ?></td>
|
<?php $gCodeP = (string) ($row->ds_gugun_code ?? ''); ?>
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_name) ?></td>
|
<td class="text-left"><?= esc((string) (($gugunNameMap[$gCodeP] ?? '') !== '' ? $gugunNameMap[$gCodeP] : $gCodeP)) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_rep_name) ?></td>
|
<td class="text-center"><?= esc($daDispP) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_biz_no) ?></td>
|
<td class="text-left"><?= esc($row->ds_zone_code ?? '') ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_va_number) ?></td>
|
<td class="text-left"><?= esc($row->ds_rep_name ?? '') ?></td>
|
||||||
<td class="text-center"><?= (int) $row->ds_state === 1 ? '정상' : ((int) $row->ds_state === 2 ? '폐업' : '직권해지') ?></td>
|
<td class="text-left"><?= esc($row->ds_name ?? '') ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_regdate ?? '') ?></td>
|
<td class="text-left"><?= esc($zipP) ?></td>
|
||||||
<td class="text-center">
|
<td class="text-left"><?= esc($addrCombinedP) ?></td>
|
||||||
<a href="<?= mgmt_url('designated-shops/edit/' . (int) $row->ds_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
|
<td class="text-left"><?= esc($row->ds_biz_no ?? '') ?></td>
|
||||||
<form action="<?= mgmt_url('designated-shops/delete/' . (int) $row->ds_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');">
|
<td class="text-left"><?= esc($row->ds_tel ?? '') ?></td>
|
||||||
<?= csrf_field() ?>
|
<td class="text-left"><?= esc($row->ds_shop_no) ?></td>
|
||||||
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
<td class="text-left"><?= esc($row->ds_va_number) ?></td>
|
||||||
</form>
|
<td class="text-center"><?= esc($stLabP) ?></td>
|
||||||
</td>
|
<td class="text-left"><?= esc($row->ds_regdate ?? '') ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
<div id="kakao-map" class="w-full border border-gray-300 mt-2" style="height:600px;"></div>
|
<div id="kakao-map" class="w-full border border-gray-300 mt-2" style="height:600px;"></div>
|
||||||
<div class="mt-2 text-sm text-gray-500">총 <?= count($shops) ?>개 판매소 표시</div>
|
<div class="mt-2 text-sm text-gray-500">총 <?= count($shops) ?>개 판매소 표시</div>
|
||||||
|
|
||||||
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=KAKAO_APP_KEY&libraries=services"></script>
|
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=<?= esc($kakaoJavascriptKey ?? '', 'attr') ?>&libraries=services"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var mapContainer = document.getElementById('kakao-map');
|
var mapContainer = document.getElementById('kakao-map');
|
||||||
if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') {
|
if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') {
|
||||||
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-400">카카오맵 API 키를 설정해 주세요.</div>';
|
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500 text-sm px-4 text-center">카카오맵을 불러올 수 없습니다. Kakao Developers → 제품 설정에서 「Kakao Map」을 켜고, 플랫폼(Web)에 이 사이트 URL을 등록했는지 확인하세요.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +1,387 @@
|
|||||||
<?= view('components/print_header', ['printTitle' => '지정판매소 현황']) ?>
|
<?php
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
$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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span>
|
<span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<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>
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 전체 현황 요약 -->
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
<div class="flex gap-4 mt-2 mb-2">
|
<form method="get" action="<?= mgmt_url('designated-shops/status') ?>" class="flex flex-wrap items-end gap-3">
|
||||||
<div class="border border-gray-300 p-3 flex-1 text-center">
|
<div>
|
||||||
<div class="text-sm text-gray-500">활성 판매소</div>
|
<label class="block text-xs text-gray-600 mb-0.5">연도</label>
|
||||||
<div class="text-2xl font-bold text-green-600"><?= number_format($totalActive) ?></div>
|
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[6rem]">
|
||||||
</div>
|
<?php foreach (($yearChoices ?? []) as $y): ?>
|
||||||
<div class="border border-gray-300 p-3 flex-1 text-center">
|
<option value="<?= (int) $y ?>" <?= $ry === (int) $y ? 'selected' : '' ?>><?= (int) $y ?>년</option>
|
||||||
<div class="text-sm text-gray-500">비활성/취소 판매소</div>
|
<?php endforeach; ?>
|
||||||
<div class="text-2xl font-bold text-red-600"><?= number_format($totalInactive) ?></div>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 p-3 flex-1 text-center">
|
<div class="min-w-[12rem]">
|
||||||
<div class="text-sm text-gray-500">전체</div>
|
<label class="block text-xs text-gray-600 mb-0.5">군·구</label>
|
||||||
<div class="text-2xl font-bold text-gray-700"><?= number_format($totalActive + $totalInactive) ?></div>
|
<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">
|
||||||
</div>
|
<?= 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>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
<?php $zoneRows = $zoneSummaryRows ?? []; ?>
|
||||||
<!-- 연도별 신규등록 -->
|
<section class="mx-2 mb-3 no-print">
|
||||||
<div>
|
<div class="text-xs font-semibold text-gray-700 mb-1">동별 현행 요약 (<?= esc($fixedGugunLabel ?? '군·구') ?>)</div>
|
||||||
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 신규등록 건수</h3>
|
<?php if (! empty($zoneRows)): ?>
|
||||||
<div class="border border-gray-300 overflow-auto">
|
<div class="flex flex-wrap gap-1 mb-2">
|
||||||
<table class="w-full data-table">
|
<?php foreach ($zoneRows as $z): ?>
|
||||||
<thead>
|
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded border border-gray-300 bg-gray-50 text-gray-700">
|
||||||
<tr>
|
<?= esc((string) $z->zone_label) ?> <?= number_format((int) $z->curr_end) ?>
|
||||||
<th>연도</th>
|
</span>
|
||||||
<th>신규등록 건수</th>
|
<?php endforeach; ?>
|
||||||
</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-4">데이터가 없습니다.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 취소/비활성 건수</h3>
|
<script>
|
||||||
<div class="border border-gray-300 overflow-auto">
|
(function () {
|
||||||
<table class="w-full data-table">
|
var tipEl = document.getElementById('ds-floating-tip');
|
||||||
<thead>
|
if (!tipEl) return;
|
||||||
<tr>
|
var badges = Array.prototype.slice.call(document.querySelectorAll('.ds-help-badge'));
|
||||||
<th>연도</th>
|
if (!badges.length) return;
|
||||||
<th>취소/비활성 건수</th>
|
|
||||||
</tr>
|
function placeTip(target) {
|
||||||
</thead>
|
var text = String(target.getAttribute('data-tip') || '').trim();
|
||||||
<tbody class="text-right">
|
if (!text) return;
|
||||||
<?php foreach ($cancelByYear as $row): ?>
|
tipEl.textContent = text;
|
||||||
<tr>
|
tipEl.style.display = 'block';
|
||||||
<td class="text-center"><?= esc($row->yr) ?>년</td>
|
tipEl.setAttribute('aria-hidden', 'false');
|
||||||
<td><?= number_format((int) $row->cnt) ?></td>
|
|
||||||
</tr>
|
var rect = target.getBoundingClientRect();
|
||||||
<?php endforeach; ?>
|
var tipRect = tipEl.getBoundingClientRect();
|
||||||
<?php if (empty($cancelByYear)): ?>
|
var vw = window.innerWidth || document.documentElement.clientWidth;
|
||||||
<tr><td colspan="2" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
var vh = window.innerHeight || document.documentElement.clientHeight;
|
||||||
<?php endif; ?>
|
var gap = 8;
|
||||||
</tbody>
|
|
||||||
</table>
|
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>
|
</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>
|
||||||
|
|||||||
@@ -13,11 +13,9 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16">번호</th>
|
<th class="w-16">번호</th>
|
||||||
<th>구분</th>
|
|
||||||
<th>대상자명</th>
|
<th>대상자명</th>
|
||||||
<th>연락처</th>
|
<th>연락처</th>
|
||||||
<th>주소</th>
|
<th>주소</th>
|
||||||
<th>동코드</th>
|
|
||||||
<th>비고</th>
|
<th>비고</th>
|
||||||
<th>종료일</th>
|
<th>종료일</th>
|
||||||
<th class="w-20">상태</th>
|
<th class="w-20">상태</th>
|
||||||
@@ -28,11 +26,9 @@
|
|||||||
<?php foreach ($list as $row): ?>
|
<?php foreach ($list as $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->fr_idx) ?></td>
|
<td class="text-center"><?= esc($row->fr_idx) ?></td>
|
||||||
<td class="text-center"><?= esc($row->fr_type_code) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->fr_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->fr_name) ?></td>
|
||||||
<td class="text-center"><?= esc($row->fr_phone) ?></td>
|
<td class="text-center"><?= esc($row->fr_phone) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->fr_addr) ?></td>
|
<td class="text-left pl-2"><?= esc($row->fr_addr) ?></td>
|
||||||
<td class="text-center"><?= esc($row->fr_dong_code) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->fr_note) ?></td>
|
<td class="text-left pl-2"><?= esc($row->fr_note) ?></td>
|
||||||
<td class="text-center"><?= esc($row->fr_end_date) ?></td>
|
<td class="text-center"><?= esc($row->fr_end_date) ?></td>
|
||||||
<td class="text-center"><?= (int) $row->fr_state === 1 ? '사용' : '미사용' ?></td>
|
<td class="text-center"><?= (int) $row->fr_state === 1 ? '사용' : '미사용' ?></td>
|
||||||
@@ -47,7 +43,7 @@
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($list)): ?>
|
<?php if (empty($list)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="text-center text-gray-500 py-4 text-sm space-y-1">
|
<td colspan="8" class="text-center text-gray-500 py-4 text-sm space-y-1">
|
||||||
<p>등록된 데이터가 없습니다.</p>
|
<p>등록된 데이터가 없습니다.</p>
|
||||||
<p class="text-gray-400">다른 지자체를 선택 중이면 해당 지자체 기준으로만 조회됩니다. Super Admin 은 상단에서 작업 지자체를 바꿔 보세요.</p>
|
<p class="text-gray-400">다른 지자체를 선택 중이면 해당 지자체 기준으로만 조회됩니다. Super Admin 은 상단에서 작업 지자체를 바꿔 보세요.</p>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -93,14 +93,10 @@ body { overflow: hidden; }
|
|||||||
<?php
|
<?php
|
||||||
$hasChildren = ! empty($navItem->children);
|
$hasChildren = ! empty($navItem->children);
|
||||||
$parentLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
|
$parentLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
|
||||||
|
$activeChild = $hasChildren ? menu_active_child_for_parent($navItem, $currentPath, []) : null;
|
||||||
$parentIsCurrent = $adminNavItemIsCurrent($navItem->mm_link ?? null);
|
$parentIsCurrent = $adminNavItemIsCurrent($navItem->mm_link ?? null);
|
||||||
if (! $parentIsCurrent && $hasChildren) {
|
if (! $parentIsCurrent && $activeChild !== null) {
|
||||||
foreach ($navItem->children as $ch) {
|
$parentIsCurrent = true;
|
||||||
if ($adminNavItemIsCurrent($ch->mm_link ?? null)) {
|
|
||||||
$parentIsCurrent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
@@ -115,7 +111,8 @@ body { overflow: hidden; }
|
|||||||
<?php foreach ($navItem->children as $child): ?>
|
<?php foreach ($navItem->children as $child): ?>
|
||||||
<?php
|
<?php
|
||||||
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
|
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
|
||||||
$childIsCurrent = $adminNavItemIsCurrent($child->mm_link ?? null);
|
$childIsCurrent = $activeChild !== null
|
||||||
|
&& (int) ($child->mm_idx ?? 0) === (int) ($activeChild->mm_idx ?? -1);
|
||||||
?>
|
?>
|
||||||
<?php if ($childLink !== ''): ?>
|
<?php if ($childLink !== ''): ?>
|
||||||
<a href="<?= base_url($childLink) ?>"
|
<a href="<?= base_url($childLink) ?>"
|
||||||
|
|||||||
@@ -11,28 +11,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">소속</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">담당자 구분 <span class="text-red-500">*</span></label>
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_dept_code">
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_category" required>
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach ($deptCodes as $cd): ?>
|
<?php foreach (($categories ?? []) as $key => $label): ?>
|
||||||
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_dept_code') === $cd->cd_code ? 'selected' : '' ?>>
|
<option value="<?= esc($key) ?>" <?= old('mg_category') === $key ? 'selected' : '' ?>>
|
||||||
<?= esc($cd->cd_name) ?>
|
<?= esc($label) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<input type="hidden" name="mg_position_code" value="<?= esc(old('mg_position_code', '')) ?>"/>
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">직위</label>
|
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_position_code">
|
|
||||||
<option value="">선택</option>
|
|
||||||
<?php foreach ($positionCodes as $cd): ?>
|
|
||||||
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_position_code') === $cd->cd_code ? 'selected' : '' ?>>
|
|
||||||
<?= esc($cd->cd_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
||||||
|
|||||||
@@ -11,28 +11,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">소속</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">담당자 구분 <span class="text-red-500">*</span></label>
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_dept_code">
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_category" required>
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach ($deptCodes as $cd): ?>
|
<?php foreach (($categories ?? []) as $key => $label): ?>
|
||||||
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_dept_code', $item->mg_dept_code) === $cd->cd_code ? 'selected' : '' ?>>
|
<option value="<?= esc($key) ?>" <?= old('mg_category', (string) ($item->mg_dept_code ?? '')) === $key ? 'selected' : '' ?>>
|
||||||
<?= esc($cd->cd_name) ?>
|
<?= esc($label) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<input type="hidden" name="mg_position_code" value="<?= esc(old('mg_position_code', $item->mg_position_code)) ?>"/>
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">직위</label>
|
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_position_code">
|
|
||||||
<option value="">선택</option>
|
|
||||||
<?php foreach ($positionCodes as $cd): ?>
|
|
||||||
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_position_code', $item->mg_position_code) === $cd->cd_code ? 'selected' : '' ?>>
|
|
||||||
<?= esc($cd->cd_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
||||||
|
|||||||
@@ -8,14 +8,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
|
<form method="GET" action="<?= mgmt_url('managers') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-600">카테고리</label>
|
||||||
|
<select name="category" class="border border-gray-300 rounded px-2 py-1 text-sm w-44">
|
||||||
|
<option value="">전 체</option>
|
||||||
|
<?php foreach (($categories ?? []) as $key => $label): ?>
|
||||||
|
<option value="<?= esc($key) ?>" <?= ($category ?? '') === $key ? 'selected' : '' ?>><?= esc($label) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
|
<a href="<?= mgmt_url('managers') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16">번호</th>
|
<th class="w-16">번호</th>
|
||||||
<th>담당자명</th>
|
<th>담당자명</th>
|
||||||
<th>소속</th>
|
<th>카테고리</th>
|
||||||
<th>직위</th>
|
|
||||||
<th>전화</th>
|
<th>전화</th>
|
||||||
<th>휴대전화</th>
|
<th>휴대전화</th>
|
||||||
<th>이메일</th>
|
<th>이메일</th>
|
||||||
@@ -28,8 +40,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->mg_idx) ?></td>
|
<td class="text-center"><?= esc($row->mg_idx) ?></td>
|
||||||
<td class="text-center"><?= esc($row->mg_name) ?></td>
|
<td class="text-center"><?= esc($row->mg_name) ?></td>
|
||||||
<td class="text-center"><?= esc($row->mg_dept_code) ?></td>
|
<td class="text-center">
|
||||||
<td class="text-center"><?= esc($row->mg_position_code) ?></td>
|
<?php
|
||||||
|
$cat = (string) ($row->mg_dept_code ?? '');
|
||||||
|
$catLabel = $categories[$cat] ?? $cat;
|
||||||
|
echo esc($catLabel);
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
<td class="text-center"><?= esc($row->mg_tel) ?></td>
|
<td class="text-center"><?= esc($row->mg_tel) ?></td>
|
||||||
<td class="text-center"><?= esc($row->mg_phone) ?></td>
|
<td class="text-center"><?= esc($row->mg_phone) ?></td>
|
||||||
<td class="text-center"><?= esc($row->mg_email) ?></td>
|
<td class="text-center"><?= esc($row->mg_email) ?></td>
|
||||||
@@ -44,7 +61,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($list)): ?>
|
<?php if (empty($list)): ?>
|
||||||
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr>
|
<tr><td colspan="8" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-32">적용시작일 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-gray-700 w-32">적용시작일 <span class="text-red-500">*</span></label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="pu_start_date" type="date" value="<?= esc(old('pu_start_date', $item->pu_start_date)) ?>" required/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="pu_start_date" type="date" value="<?= esc(old('pu_start_date', date('Y-m-d'))) ?>" required/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
@@ -6,11 +6,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
|
<?php
|
||||||
|
$fieldLabelMap = [
|
||||||
|
'pu_box_per_pack' => '박스당 팩 수',
|
||||||
|
'pu_pack_per_sheet' => '팩당 낱장 수',
|
||||||
|
'pu_start_date' => '적용시작일',
|
||||||
|
'pu_end_date' => '적용종료일',
|
||||||
|
'pu_state' => '상태',
|
||||||
|
];
|
||||||
|
?>
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16">번호</th>
|
<th class="w-16">번호</th>
|
||||||
<th>변경 필드</th>
|
<th>변경 내용</th>
|
||||||
<th>이전 값</th>
|
<th>이전 값</th>
|
||||||
<th>변경 값</th>
|
<th>변경 값</th>
|
||||||
<th>변경일시</th>
|
<th>변경일시</th>
|
||||||
@@ -20,7 +29,7 @@
|
|||||||
<?php foreach ($list as $row): ?>
|
<?php foreach ($list as $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->puh_idx) ?></td>
|
<td class="text-center"><?= esc($row->puh_idx) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->puh_field) ?></td>
|
<td class="text-left pl-2"><?= esc($fieldLabelMap[(string) $row->puh_field] ?? $row->puh_field) ?></td>
|
||||||
<td><?= esc($row->puh_old_value) ?></td>
|
<td><?= esc($row->puh_old_value) ?></td>
|
||||||
<td><?= esc($row->puh_new_value) ?></td>
|
<td><?= esc($row->puh_new_value) ?></td>
|
||||||
<td class="text-center"><?= esc($row->puh_changed_at) ?></td>
|
<td class="text-center"><?= esc($row->puh_changed_at) ?></td>
|
||||||
|
|||||||
@@ -35,9 +35,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php foreach ($list as $row): ?>
|
<?php
|
||||||
|
$startNo = 1;
|
||||||
|
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
||||||
|
$currentPage = (int) $pager->getCurrentPage();
|
||||||
|
$perPage = (int) $pager->getPerPage();
|
||||||
|
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php foreach (($list ?? []) as $idx => $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->pu_idx) ?></td>
|
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
||||||
<td class="text-center font-mono"><?= esc($row->pu_bag_code) ?></td>
|
<td class="text-center font-mono"><?= esc($row->pu_bag_code) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->pu_bag_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->pu_bag_name) ?></td>
|
||||||
<td><?= number_format((int) $row->pu_box_per_pack) ?></td>
|
<td><?= number_format((int) $row->pu_box_per_pack) ?></td>
|
||||||
|
|||||||
@@ -44,9 +44,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($list as $row): ?>
|
<?php
|
||||||
|
$startNo = 1;
|
||||||
|
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
||||||
|
$currentPage = (int) $pager->getCurrentPage();
|
||||||
|
$perPage = (int) $pager->getPerPage();
|
||||||
|
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php foreach (($list ?? []) as $idx => $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->sa_idx) ?></td>
|
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->sa_kind ?? '') ?></td>
|
<td class="text-left pl-2"><?= esc($row->sa_kind ?? '') ?></td>
|
||||||
<td class="text-center"><?= esc($row->sa_code ?? '') ?></td>
|
<td class="text-center"><?= esc($row->sa_code ?? '') ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->sa_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->sa_name) ?></td>
|
||||||
|
|||||||
@@ -2,62 +2,144 @@
|
|||||||
/** @var list<object> $codeKinds */
|
/** @var list<object> $codeKinds */
|
||||||
/** @var array<int,int> $countMap */
|
/** @var array<int,int> $countMap */
|
||||||
/** @var bool $canManageKinds */
|
/** @var bool $canManageKinds */
|
||||||
$canManageKinds = ! empty($canManageKinds);
|
/** @var bool $canManageDetails */
|
||||||
$showKindActions = $canManageKinds;
|
/** @var object|null $selectedKind */
|
||||||
$colCount = 6 + ($showKindActions ? 1 : 0);
|
/** @var list<object> $detailList */
|
||||||
|
/** @var array<int,bool> $rowCanEdit */
|
||||||
|
$canManageKinds = ! empty($canManageKinds);
|
||||||
|
$canManageDetails = ! empty($canManageDetails);
|
||||||
|
$showKindActions = $canManageKinds;
|
||||||
|
$selectedKindId = (int) ($selectedKind->ck_idx ?? 0);
|
||||||
|
$colCount = 6 + ($showKindActions ? 1 : 0);
|
||||||
|
$detailColCount = 7 + ($canManageDetails ? 1 : 0);
|
||||||
?>
|
?>
|
||||||
<div class="space-y-4">
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
<section>
|
<section>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-2 border-b pb-1">
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-2 border-b pb-1">
|
||||||
<h3 class="text-base font-bold text-gray-700">기본코드 종류</h3>
|
<h3 class="text-base font-bold text-gray-700">기본코드 종류</h3>
|
||||||
<div class="flex flex-wrap items-center gap-2 text-xs sm:text-sm">
|
<div class="flex flex-wrap items-center gap-2 text-xs sm:text-sm">
|
||||||
<?php if ($canManageKinds): ?>
|
<?php if ($canManageKinds): ?>
|
||||||
<a href="<?= base_url('admin/code-kinds/create') ?>" class="inline-flex items-center rounded bg-[#1c4e80] px-3 py-1.5 text-white shadow hover:opacity-90">코드 종류 등록</a>
|
<a href="<?= base_url('admin/code-kinds/create') ?>" class="inline-flex items-center rounded bg-[#1c4e80] px-3 py-1.5 text-white shadow hover:opacity-90">코드 종류 등록</a>
|
||||||
<?php elseif (! $canManageKinds): ?>
|
<?php else: ?>
|
||||||
<span class="text-gray-500">코드 종류 등록·수정은 super admin·본부 관리자만 가능합니다. 세부코드는 행의 링크에서 조회할 수 있습니다.</span>
|
<span class="text-gray-500">코드 종류 등록·수정은 super admin·본부 관리자만 가능합니다.</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="data-table">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<thead><tr>
|
<table class="data-table w-full">
|
||||||
<th class="w-14"><?= $showKindActions ? 'PK' : '번호' ?></th>
|
<thead><tr>
|
||||||
<th class="w-24">코드</th>
|
<th class="w-14"><?= $showKindActions ? 'PK' : '번호' ?></th>
|
||||||
<th>코드명</th>
|
<th class="w-24">코드</th>
|
||||||
<th class="w-28">세부코드</th>
|
<th>코드명</th>
|
||||||
<th class="w-20">상태</th>
|
<th class="w-24">세부건수</th>
|
||||||
<th class="w-40">등록일</th>
|
<th class="w-20">상태</th>
|
||||||
<?php if ($showKindActions): ?>
|
<th class="w-40">등록일</th>
|
||||||
<th class="w-44">작업</th>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (! empty($codeKinds)): ?>
|
|
||||||
<?php $i = 0; foreach ($codeKinds as $row): $i++; ?>
|
|
||||||
<tr>
|
|
||||||
<td class="text-center"><?= $showKindActions ? esc((string) $row->ck_idx) : (string) $i ?></td>
|
|
||||||
<td class="text-center font-mono"><?= esc($row->ck_code) ?></td>
|
|
||||||
<td><?= esc($row->ck_name) ?></td>
|
|
||||||
<td class="text-center">
|
|
||||||
<a href="<?= base_url('bag/code-details/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline"><?= (int) ($countMap[$row->ck_idx] ?? 0) ?>개 보기</a>
|
|
||||||
</td>
|
|
||||||
<td class="text-center"><?= (int) ($row->ck_state ?? 0) === 1 ? '사용' : '미사용' ?></td>
|
|
||||||
<td class="text-left"><?= esc($row->ck_regdate ?? '') ?></td>
|
|
||||||
<?php if ($showKindActions): ?>
|
<?php if ($showKindActions): ?>
|
||||||
<td class="text-center text-sm">
|
<th class="w-36">작업</th>
|
||||||
<a href="<?= base_url('bag/code-details/' . (int) $row->ck_idx) ?>" class="text-green-600 hover:underline mr-1">세부코드</a>
|
|
||||||
<a href="<?= base_url('admin/code-kinds/edit/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline mr-1">수정</a>
|
|
||||||
<form action="<?= base_url('admin/code-kinds/delete/' . (int) $row->ck_idx) ?>" method="POST" class="inline" onsubmit="return confirm('이 코드 종류를 삭제하시겠습니까?');">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<button type="submit" class="text-red-600 hover:underline">삭제</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tr>
|
</tr></thead>
|
||||||
<?php endforeach; ?>
|
<tbody>
|
||||||
<?php else: ?>
|
<?php if (! empty($codeKinds)): ?>
|
||||||
<tr><td colspan="<?= (string) $colCount ?>" class="text-center text-gray-400 py-4">등록된 코드 종류가 없습니다.</td></tr>
|
<?php $i = 0; foreach ($codeKinds as $row): $i++; ?>
|
||||||
|
<?php
|
||||||
|
$isSelected = (int) $row->ck_idx === $selectedKindId;
|
||||||
|
$detailUrl = base_url('bag/code-kinds?ck_idx=' . (int) $row->ck_idx);
|
||||||
|
?>
|
||||||
|
<tr class="<?= $isSelected ? 'bg-blue-50' : '' ?> cursor-pointer hover:bg-blue-50"
|
||||||
|
onclick="window.location.href='<?= esc($detailUrl, 'attr') ?>'">
|
||||||
|
<td class="text-center"><?= $showKindActions ? esc((string) $row->ck_idx) : (string) $i ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->ck_code) ?></td>
|
||||||
|
<td><?= esc($row->ck_name) ?></td>
|
||||||
|
<td class="text-center"><?= (int) ($countMap[$row->ck_idx] ?? 0) ?>개</td>
|
||||||
|
<td class="text-center"><?= (int) ($row->ck_state ?? 0) === 1 ? '사용' : '미사용' ?></td>
|
||||||
|
<td class="text-left"><?= esc($row->ck_regdate ?? '') ?></td>
|
||||||
|
<?php if ($showKindActions): ?>
|
||||||
|
<td class="text-center text-sm" onclick="event.stopPropagation()">
|
||||||
|
<a href="<?= base_url('admin/code-kinds/edit/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline mr-1">수정</a>
|
||||||
|
<form action="<?= base_url('admin/code-kinds/delete/' . (int) $row->ck_idx) ?>" method="POST" class="inline" onsubmit="return confirm('이 코드 종류를 삭제하시겠습니까?');">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit" class="text-red-600 hover:underline">삭제</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<tr><td colspan="<?= (string) $colCount ?>" class="text-center text-gray-400 py-4">등록된 코드 종류가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-2 border-b pb-1">
|
||||||
|
<h3 class="text-base font-bold text-gray-700">
|
||||||
|
세부코드
|
||||||
|
<?php if ($selectedKind !== null): ?>
|
||||||
|
— <?= esc($selectedKind->ck_name) ?> (<?= esc($selectedKind->ck_code) ?>)
|
||||||
|
<?php endif; ?>
|
||||||
|
</h3>
|
||||||
|
<?php if ($canManageDetails && $selectedKind !== null): ?>
|
||||||
|
<a href="<?= base_url('admin/code-details/' . (int) $selectedKind->ck_idx . '/create') ?>" class="inline-flex items-center rounded bg-[#1c4e80] px-3 py-1.5 text-white shadow hover:opacity-90 text-sm">세부코드 등록</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
|
<?php if ($selectedKind === null): ?>
|
||||||
|
<div class="border border-gray-300 rounded p-6 text-center text-gray-500">왼쪽에서 코드 종류를 선택해 주세요.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="border border-gray-300 overflow-auto">
|
||||||
|
<table class="data-table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-16">번호</th>
|
||||||
|
<th class="w-24">코드</th>
|
||||||
|
<th>코드명</th>
|
||||||
|
<th class="w-24">범위</th>
|
||||||
|
<th class="w-20">정렬</th>
|
||||||
|
<th class="w-20">상태</th>
|
||||||
|
<th class="w-40">등록일</th>
|
||||||
|
<?php if ($canManageDetails): ?>
|
||||||
|
<th class="w-28">작업</th>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (! empty($detailList)): ?>
|
||||||
|
<?php foreach ($detailList as $row): ?>
|
||||||
|
<?php
|
||||||
|
$isPlatform = (($row->cd_source ?? 'platform') === 'platform' && (int) ($row->cd_lg_idx ?? 0) === 0);
|
||||||
|
$scopeLabel = $isPlatform ? '공통' : '지자체';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc((string) $row->cd_idx) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->cd_code) ?></td>
|
||||||
|
<td><?= esc($row->cd_name) ?></td>
|
||||||
|
<td class="text-center text-xs"><?= esc($scopeLabel) ?></td>
|
||||||
|
<td class="text-center"><?= (int) ($row->cd_sort ?? 0) ?></td>
|
||||||
|
<td class="text-center"><?= (int) ($row->cd_state ?? 0) === 1 ? '사용' : '미사용' ?></td>
|
||||||
|
<td class="text-left"><?= esc($row->cd_regdate ?? '') ?></td>
|
||||||
|
<?php if ($canManageDetails): ?>
|
||||||
|
<td class="text-center text-sm">
|
||||||
|
<?php if (! empty($rowCanEdit[$row->cd_idx])): ?>
|
||||||
|
<a href="<?= base_url('admin/code-details/edit/' . (int) $row->cd_idx) ?>" class="text-blue-600 hover:underline">수정</a>
|
||||||
|
<form action="<?= base_url('admin/code-details/delete/' . (int) $row->cd_idx) ?>" method="POST" class="ml-1 inline" onsubmit="return confirm('이 세부코드를 삭제하시겠습니까?');">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit" class="text-red-600 hover:underline">삭제</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-gray-400">—</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<tr><td colspan="<?= (string) $detailColCount ?>" class="text-center text-gray-400 py-4">등록된 세부코드가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -133,10 +133,13 @@ $userNav = session_user_nav_display();
|
|||||||
<?php if (! empty($navItem->children)): ?>
|
<?php if (! empty($navItem->children)): ?>
|
||||||
<div class="absolute left-0 top-full z-[200] -mt-1 pt-2 hidden group-hover:block group-focus-within:block min-w-[12rem]">
|
<div class="absolute left-0 top-full z-[200] -mt-1 pt-2 hidden group-hover:block group-focus-within:block min-w-[12rem]">
|
||||||
<div class="bg-white border border-gray-200 rounded shadow-lg py-1">
|
<div class="bg-white border border-gray-200 rounded shadow-lg py-1">
|
||||||
|
<?php
|
||||||
|
$activeChild = site_nav_active_child_for_parent($navItem, $currentPath, $dashboardPathAliases);
|
||||||
|
?>
|
||||||
<?php foreach ($navItem->children as $child): ?>
|
<?php foreach ($navItem->children as $child): ?>
|
||||||
<?php
|
<?php
|
||||||
$childLink = site_nav_resolved_link_path($child->mm_link ?? null, $child->mm_name ?? null);
|
$childLink = site_nav_resolved_link_path($child->mm_link ?? null, $child->mm_name ?? null);
|
||||||
$childCurrent = menu_link_matches_request($child->mm_link ?? null, $currentPath, $dashboardPathAliases);
|
$childCurrent = $activeChild !== null && $child === $activeChild;
|
||||||
?>
|
?>
|
||||||
<?php if ($childLink !== ''): ?>
|
<?php if ($childLink !== ''): ?>
|
||||||
<a href="<?= base_url($childLink) ?>"
|
<a href="<?= base_url($childLink) ?>"
|
||||||
|
|||||||
@@ -71,14 +71,12 @@ body { overflow: hidden; }
|
|||||||
<?php foreach ($siteNavTree as $navItem): ?>
|
<?php foreach ($siteNavTree as $navItem): ?>
|
||||||
<?php
|
<?php
|
||||||
$navLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
|
$navLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
|
||||||
|
$activeChild = ! empty($navItem->children)
|
||||||
|
? menu_active_child_for_parent($navItem, $currentPath, $dashboardPathAliases)
|
||||||
|
: null;
|
||||||
$isActive = site_nav_link_matches_current($navItem->mm_link ?? null, $currentPath, $dashboardPathAliases);
|
$isActive = site_nav_link_matches_current($navItem->mm_link ?? null, $currentPath, $dashboardPathAliases);
|
||||||
if (! $isActive && ! empty($navItem->children)) {
|
if (! $isActive && $activeChild !== null) {
|
||||||
foreach ($navItem->children as $ch) {
|
$isActive = true;
|
||||||
if (site_nav_link_matches_current($ch->mm_link ?? null, $currentPath, $dashboardPathAliases)) {
|
|
||||||
$isActive = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
@@ -93,7 +91,8 @@ body { overflow: hidden; }
|
|||||||
<?php foreach ($navItem->children as $child): ?>
|
<?php foreach ($navItem->children as $child): ?>
|
||||||
<?php
|
<?php
|
||||||
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
|
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
|
||||||
$childCurrent = menu_link_matches_request($child->mm_link ?? null, $currentPath, $dashboardPathAliases);
|
$childCurrent = $activeChild !== null
|
||||||
|
&& (int) ($child->mm_idx ?? 0) === (int) ($activeChild->mm_idx ?? -1);
|
||||||
?>
|
?>
|
||||||
<?php if ($childLink !== ''): ?>
|
<?php if ($childLink !== ''): ?>
|
||||||
<a href="<?= base_url($childLink) ?>"
|
<a href="<?= base_url($childLink) ?>"
|
||||||
|
|||||||
126
app/Views/components/kakao_address_search.php
Normal file
126
app/Views/components/kakao_address_search.php
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/** @var string $buttonId 주소 검색 버튼 id */
|
||||||
|
$buttonId = $buttonId ?? 'btn-kakao-postcode';
|
||||||
|
/** @var string $zipName 우편번호 input name */
|
||||||
|
$zipName = $zipName ?? 'ds_zip';
|
||||||
|
/** @var string $roadName 도로명 input name */
|
||||||
|
$roadName = $roadName ?? 'ds_addr';
|
||||||
|
/** @var string $jibunName 지번 input name */
|
||||||
|
$jibunName = $jibunName ?? 'ds_addr_jibun';
|
||||||
|
/** @var string $sidoFieldName 카카오 시·도 → hidden name (비우면 미설정) */
|
||||||
|
$sidoFieldName = $sidoFieldName ?? '';
|
||||||
|
/** @var string $sigunguFieldName 카카오 시·군·구 → hidden name */
|
||||||
|
$sigunguFieldName = $sigunguFieldName ?? '';
|
||||||
|
/** @var string $detailFieldName 상세주소 input name (건물명 등, 비우면 미사용) */
|
||||||
|
$detailFieldName = $detailFieldName ?? '';
|
||||||
|
/**
|
||||||
|
* @var array{lg_sido?: string, lg_gugun?: string}|null $tenantScope 지자체 관할 검사(비우면 미검사)
|
||||||
|
*/
|
||||||
|
$tenantScope = $tenantScope ?? null;
|
||||||
|
/** @var bool $roadBaseOnly true면 도로명에 건물명 괄호 미부착(상세로 이전) */
|
||||||
|
$roadBaseOnly = ! empty($roadBaseOnly);
|
||||||
|
?>
|
||||||
|
<script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var btnId = <?= json_encode($buttonId, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var zipName = <?= json_encode($zipName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var roadName = <?= json_encode($roadName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var jibunName = <?= json_encode($jibunName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var sidoFieldName = <?= json_encode($sidoFieldName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var sigunguFieldName = <?= json_encode($sigunguFieldName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var detailFieldName = <?= json_encode($detailFieldName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var roadBaseOnly = <?= $roadBaseOnly ? 'true' : 'false' ?>;
|
||||||
|
var tenantScope = <?= json_encode($tenantScope ?? (object) [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
|
||||||
|
function compactStr(s) {
|
||||||
|
return String(s || '').replace(/\s+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenMatches(needle, primary, blob) {
|
||||||
|
var n = compactStr(needle);
|
||||||
|
if (!n) return true;
|
||||||
|
var b = compactStr(blob);
|
||||||
|
if (b.indexOf(n) !== -1) return true;
|
||||||
|
var p = compactStr(primary);
|
||||||
|
if (p.indexOf(n) !== -1) return true;
|
||||||
|
if (n.indexOf(p) !== -1 && p) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addressAllowedByTenant(data) {
|
||||||
|
var lgSido = tenantScope && tenantScope.lg_sido ? String(tenantScope.lg_sido) : '';
|
||||||
|
var lgGugun = tenantScope && tenantScope.lg_gugun ? String(tenantScope.lg_gugun) : '';
|
||||||
|
if (!lgSido && !lgGugun) return true;
|
||||||
|
var sido = data.sido || '';
|
||||||
|
var sigungu = data.sigungu || '';
|
||||||
|
var road = data.roadAddress || '';
|
||||||
|
var jibun = data.jibunAddress || '';
|
||||||
|
var zip = data.zonecode || '';
|
||||||
|
var blob = sido + ' ' + sigungu + ' ' + road + ' ' + jibun + ' ' + zip;
|
||||||
|
if (lgSido && !tokenMatches(lgSido, sido, blob)) return false;
|
||||||
|
if (lgGugun && !tokenMatches(lgGugun, sigungu, blob)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind() {
|
||||||
|
var btn = document.getElementById(btnId);
|
||||||
|
if (!btn) return;
|
||||||
|
var form = btn.closest('form');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
function field(n) {
|
||||||
|
return form.querySelector('[name="' + n + '"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (typeof daum === 'undefined' || !daum.Postcode) {
|
||||||
|
window.alert('주소 검색 스크립트를 불러오지 못했습니다. 네트워크를 확인해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new daum.Postcode({
|
||||||
|
oncomplete: function (data) {
|
||||||
|
if (!addressAllowedByTenant(data)) {
|
||||||
|
window.alert('작업 중인 지자체 관할이 아닌 주소입니다. 해당 시·구 주소를 검색해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var zipEl = field(zipName);
|
||||||
|
var roadEl = field(roadName);
|
||||||
|
var jibunEl = field(jibunName);
|
||||||
|
if (zipEl) zipEl.value = data.zonecode || '';
|
||||||
|
|
||||||
|
var roadAddr = data.roadAddress || '';
|
||||||
|
if (!roadBaseOnly && data.buildingName !== '') {
|
||||||
|
roadAddr += (roadAddr !== '' ? ' (' + data.buildingName + ')' : data.buildingName);
|
||||||
|
}
|
||||||
|
if (roadEl) roadEl.value = roadAddr;
|
||||||
|
|
||||||
|
if (jibunEl) jibunEl.value = data.jibunAddress || '';
|
||||||
|
|
||||||
|
if (sidoFieldName) {
|
||||||
|
var sidoEl = field(sidoFieldName);
|
||||||
|
if (sidoEl) sidoEl.value = data.sido || '';
|
||||||
|
}
|
||||||
|
if (sigunguFieldName) {
|
||||||
|
var sigEl = field(sigunguFieldName);
|
||||||
|
if (sigEl) sigEl.value = data.sigungu || '';
|
||||||
|
}
|
||||||
|
if (detailFieldName && roadBaseOnly) {
|
||||||
|
var detEl = field(detailFieldName);
|
||||||
|
if (detEl) detEl.value = data.buildingName || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', bind);
|
||||||
|
} else {
|
||||||
|
bind();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
47
app/Views/components/kakao_map_link_button.php
Normal file
47
app/Views/components/kakao_map_link_button.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/** @var string $buttonId 버튼 id (폼마다 고유) */
|
||||||
|
$buttonId = $buttonId ?? 'btn-kakao-map-open';
|
||||||
|
/** @var string $label 버튼 텍스트 */
|
||||||
|
$label = $label ?? '지도';
|
||||||
|
?>
|
||||||
|
<button type="button" id="<?= esc($buttonId, 'attr') ?>" class="no-print border border-btn-print-border text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition shrink-0" title="카카오맵에서 이 주소 검색"><?= esc($label) ?></button>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var bid = <?= json_encode($buttonId, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
function bind() {
|
||||||
|
var btn = document.getElementById(bid);
|
||||||
|
if (!btn) return;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var form = btn.closest('form');
|
||||||
|
if (!form) return;
|
||||||
|
function val(name) {
|
||||||
|
var el = form.querySelector('[name="' + name + '"]');
|
||||||
|
return el ? String(el.value || '').trim() : '';
|
||||||
|
}
|
||||||
|
var road = val('ds_addr');
|
||||||
|
var jibun = val('ds_addr_jibun');
|
||||||
|
var detail = val('ds_addr_detail');
|
||||||
|
var q = road || jibun;
|
||||||
|
if (detail) {
|
||||||
|
q = q ? (q + ' ' + detail) : detail;
|
||||||
|
}
|
||||||
|
if (!q) {
|
||||||
|
window.alert('주소 검색으로 도로명·지번을 먼저 입력한 뒤 지도를 열 수 있습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window.openDesignatedShopKakaoMap === 'function') {
|
||||||
|
window.openDesignatedShopKakaoMap(q);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open('https://map.kakao.com/link/search/' + encodeURIComponent(q), '_blank', 'noopener,noreferrer');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', bind);
|
||||||
|
} else {
|
||||||
|
bind();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
153
app/Views/components/kakao_map_modal.php
Normal file
153
app/Views/components/kakao_map_modal.php
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$key = trim((string) ($kakaoJavascriptKey ?? ''));
|
||||||
|
?>
|
||||||
|
<div id="kakao-map-modal" class="hidden fixed inset-0 z-[300] flex items-center justify-center p-4" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="kakao-map-modal-title">
|
||||||
|
<div class="absolute inset-0 bg-black/50" id="kakao-map-modal-backdrop"></div>
|
||||||
|
<div class="relative z-[301] w-full max-w-2xl max-h-[90vh] flex flex-col rounded border border-gray-300 bg-white shadow-lg overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 bg-gray-50 shrink-0">
|
||||||
|
<span id="kakao-map-modal-title" class="text-sm font-bold text-gray-800">위치</span>
|
||||||
|
<button type="button" id="kakao-map-modal-close" class="text-gray-600 hover:text-gray-900 text-xl leading-none px-1" aria-label="닫기">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="kakao-map-modal-container" class="w-full bg-gray-100" style="min-height: 380px; height: 50vh;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var APP_KEY = <?= json_encode($key, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
|
var modal = document.getElementById('kakao-map-modal');
|
||||||
|
var backdrop = document.getElementById('kakao-map-modal-backdrop');
|
||||||
|
var btnClose = document.getElementById('kakao-map-modal-close');
|
||||||
|
var mapContainer = document.getElementById('kakao-map-modal-container');
|
||||||
|
var mapInstance = null;
|
||||||
|
var markerInstance = null;
|
||||||
|
var scriptLoading = false;
|
||||||
|
var pendingAfterLoad = [];
|
||||||
|
|
||||||
|
function hideModal() {
|
||||||
|
if (!modal) return;
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal() {
|
||||||
|
if (!modal) return;
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.setAttribute('aria-hidden', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPending() {
|
||||||
|
var q = pendingAfterLoad;
|
||||||
|
pendingAfterLoad = [];
|
||||||
|
q.forEach(function (fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureScript(cb) {
|
||||||
|
if (!APP_KEY) {
|
||||||
|
window.alert('카카오맵 JavaScript 키가 설정되지 않았습니다. .env에 kakao.javascriptKey를 설정해 주세요. (Kakao Developers → 앱 키 → JavaScript 키)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof kakao !== 'undefined' && kakao.maps) {
|
||||||
|
cb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingAfterLoad.push(cb);
|
||||||
|
if (scriptLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scriptLoading = true;
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.charset = 'UTF-8';
|
||||||
|
s.async = true;
|
||||||
|
// 동적 삽입 시 autoload=false 후 kakao.maps.load() 필수 (카카오 웹 가이드)
|
||||||
|
s.src = 'https://dapi.kakao.com/v2/maps/sdk.js?appkey=' + encodeURIComponent(APP_KEY) + '&libraries=services&autoload=false';
|
||||||
|
s.onload = function () {
|
||||||
|
scriptLoading = false;
|
||||||
|
if (typeof kakao === 'undefined' || !kakao.maps || typeof kakao.maps.load !== 'function') {
|
||||||
|
pendingAfterLoad = [];
|
||||||
|
window.alert(
|
||||||
|
'카카오맵 API를 불러올 수 없습니다.\n\n' +
|
||||||
|
'Kakao Developers → 내 애플리케이션 → 해당 앱 → 「제품 설정」에서 「Kakao Map」(지도) / 로컬 API를 사용 설정으로 켜 주세요.\n' +
|
||||||
|
'(비활성 시 서버에서 OPEN_MAP_AND_LOCAL 오류가 납니다.)\n\n' +
|
||||||
|
'또한 플랫폼(Web)에 이 사이트 주소(예: http://localhost:8080)가 등록되어 있어야 합니다.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kakao.maps.load(function () {
|
||||||
|
runPending();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
s.onerror = function () {
|
||||||
|
scriptLoading = false;
|
||||||
|
pendingAfterLoad = [];
|
||||||
|
window.alert(
|
||||||
|
'카카오맵 스크립트를 불러오지 못했습니다.\n\n' +
|
||||||
|
'• 네트워크·차단(광고 차단) 확인\n' +
|
||||||
|
'• Kakao Developers → 제품 설정에서 「Kakao Map」활성화\n' +
|
||||||
|
'• 플랫폼(Web)에 접속 중인 URL 등록'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnClose) {
|
||||||
|
btnClose.addEventListener('click', hideModal);
|
||||||
|
}
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.addEventListener('click', hideModal);
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key !== 'Escape' || !modal || modal.classList.contains('hidden')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hideModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.openDesignatedShopKakaoMap = function (addressQuery) {
|
||||||
|
var q = String(addressQuery || '').trim();
|
||||||
|
if (!q) {
|
||||||
|
window.alert('주소가 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureScript(function () {
|
||||||
|
if (typeof kakao === 'undefined' || !kakao.maps || !kakao.maps.services) {
|
||||||
|
window.alert('카카오맵을 초기화할 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var geocoder = new kakao.maps.services.Geocoder();
|
||||||
|
geocoder.addressSearch(q, function (result, status) {
|
||||||
|
if (status !== kakao.maps.services.Status.OK || !result || !result[0]) {
|
||||||
|
window.alert('주소를 지도에서 찾을 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var coords = new kakao.maps.LatLng(result[0].y, result[0].x);
|
||||||
|
showModal();
|
||||||
|
if (!mapInstance) {
|
||||||
|
mapInstance = new kakao.maps.Map(mapContainer, {
|
||||||
|
center: coords,
|
||||||
|
level: 3
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mapInstance.setCenter(coords);
|
||||||
|
mapInstance.setLevel(3);
|
||||||
|
}
|
||||||
|
if (markerInstance) {
|
||||||
|
markerInstance.setMap(null);
|
||||||
|
}
|
||||||
|
markerInstance = new kakao.maps.Marker({ position: coords, map: mapInstance });
|
||||||
|
setTimeout(function () {
|
||||||
|
if (mapInstance) {
|
||||||
|
mapInstance.relayout();
|
||||||
|
mapInstance.setCenter(coords);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -41,7 +41,31 @@ test.describe('관리자 패널 — 지자체관리자', () => {
|
|||||||
|
|
||||||
test('지정판매소 목록 접근', async ({ page }) => {
|
test('지정판매소 목록 접근', async ({ page }) => {
|
||||||
await page.goto('/bag/designated-shops');
|
await page.goto('/bag/designated-shops');
|
||||||
await expect(page).toHaveURL(/\/admin\/designated-shops/);
|
await expect(page).toHaveURL(/\/bag\/designated-shops$/);
|
||||||
|
await expect(page.getByText('지정판매소 관리').first()).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: '지정판매소 등록' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('지정판매소 조회 전용(browse) 접근', async ({ page }) => {
|
||||||
|
await page.goto('/bag/designated-shops/browse');
|
||||||
|
await expect(page).toHaveURL(/\/bag\/designated-shops\/browse/);
|
||||||
|
await expect(page.getByText('지정판매소 조회').first()).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: '지정판매소 등록' })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('지정판매소 소메뉴는 현재 경로 1개만 활성화', async ({ page }) => {
|
||||||
|
const activeSubmenu = page.locator('nav a.text-blue-700.font-semibold.bg-blue-50');
|
||||||
|
|
||||||
|
await page.goto('/bag/designated-shops');
|
||||||
|
await expect(page).toHaveURL(/\/bag\/designated-shops$/);
|
||||||
|
await expect(activeSubmenu.filter({ hasText: '지정판매소 관리' })).toHaveCount(1);
|
||||||
|
await expect(activeSubmenu.filter({ hasText: '지정판매소 바코드출력' })).toHaveCount(0);
|
||||||
|
await expect(activeSubmenu.filter({ hasText: '지정판매소 조회' })).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.goto('/bag/designated-shops/browse');
|
||||||
|
await expect(page).toHaveURL(/\/bag\/designated-shops\/browse/);
|
||||||
|
await expect(activeSubmenu.filter({ hasText: '지정판매소 조회' })).toHaveCount(1);
|
||||||
|
await expect(activeSubmenu.filter({ hasText: '지정판매소 관리' })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('지자체 관리는 Super Admin 전용 — 지자체관리자 접근 시 리다이렉트', async ({ page }) => {
|
test('지자체 관리는 Super Admin 전용 — 지자체관리자 접근 시 리다이렉트', async ({ page }) => {
|
||||||
|
|||||||
@@ -60,7 +60,12 @@ const BAG_PATHS = [
|
|||||||
'/bag/free-recipients',
|
'/bag/free-recipients',
|
||||||
'/bag/free-recipients/create',
|
'/bag/free-recipients/create',
|
||||||
'/bag/designated-shops',
|
'/bag/designated-shops',
|
||||||
|
'/bag/designated-shops/browse',
|
||||||
'/bag/designated-shops/status',
|
'/bag/designated-shops/status',
|
||||||
|
'/bag/designated-shops/status/export?year=2026&ds_gugun_code=&granularity=gugun',
|
||||||
|
'/bag/designated-shops/district-new-cancel',
|
||||||
|
'/bag/designated-shops/district-new-cancel/export?year=2026',
|
||||||
|
'/bag/designated-shops/barcode',
|
||||||
'/bag/bag-prices',
|
'/bag/bag-prices',
|
||||||
'/bag/bag-prices/create',
|
'/bag/bag-prices/create',
|
||||||
'/bag/packaging-units/manage',
|
'/bag/packaging-units/manage',
|
||||||
|
|||||||
@@ -85,13 +85,15 @@ test.describe('P2-15: 지정판매소 다조건 조회', () => {
|
|||||||
await loginAsLocal(page);
|
await loginAsLocal(page);
|
||||||
await page.goto('/bag/designated-shops?ds_name=CU');
|
await page.goto('/bag/designated-shops?ds_name=CU');
|
||||||
await expect(page).toHaveURL(/ds_name=CU/);
|
await expect(page).toHaveURL(/ds_name=CU/);
|
||||||
await expect(page.locator('table.data-table')).toBeVisible();
|
await expect(page.locator('input[name="ds_name"]')).toHaveValue('CU');
|
||||||
|
await expect(page.locator('#ds-list-body')).toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('상태 필터', async ({ page }) => {
|
test('상태 필터', async ({ page }) => {
|
||||||
await loginAsLocal(page);
|
await loginAsLocal(page);
|
||||||
await page.goto('/bag/designated-shops?ds_state=1');
|
await page.goto('/bag/designated-shops?ds_state=1');
|
||||||
await expect(page.locator('table.data-table')).toBeVisible();
|
await expect(page.locator('select[name="ds_state"]')).toHaveValue('1');
|
||||||
|
await expect(page.locator('#ds-list-body')).toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('검색 폼에서 이름 입력 후 조회', async ({ page }) => {
|
test('검색 폼에서 이름 입력 후 조회', async ({ page }) => {
|
||||||
@@ -126,6 +128,17 @@ test.describe('P2-18: 지정판매소 현황', () => {
|
|||||||
await page.goto('/bag/designated-shops/status');
|
await page.goto('/bag/designated-shops/status');
|
||||||
await expect(page).toHaveURL(/\/status/);
|
await expect(page).toHaveURL(/\/status/);
|
||||||
await expect(page.locator('table.data-table').first()).toBeVisible();
|
await expect(page.locator('table.data-table').first()).toBeVisible();
|
||||||
|
await expect(page.getByRole('columnheader', { name: '종전(전년도말)' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: '엑셀저장' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GBMS형 신규/취소 현황(구·군 고정)', async ({ page }) => {
|
||||||
|
await loginAsLocal(page);
|
||||||
|
await page.goto('/bag/designated-shops/district-new-cancel');
|
||||||
|
await expect(page).toHaveURL(/district-new-cancel/);
|
||||||
|
await expect(page.locator('.gbms-dnc-table')).toBeVisible();
|
||||||
|
await expect(page.getByRole('columnheader', { name: '군·구' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: '엑셀저장' })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -279,9 +292,9 @@ test.describe('사이트 메뉴 CRUD 동작', () => {
|
|||||||
test.describe('엑셀 내보내기 다운로드', () => {
|
test.describe('엑셀 내보내기 다운로드', () => {
|
||||||
test('지정판매소 엑셀', async ({ page }) => {
|
test('지정판매소 엑셀', async ({ page }) => {
|
||||||
await loginAsLocal(page);
|
await loginAsLocal(page);
|
||||||
await page.goto('/bag/designated-shops');
|
await page.goto('/bag/designated-shops/browse');
|
||||||
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
|
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
|
||||||
await page.locator('a[href*="export"]').first().click();
|
await page.locator('a[href*="designated-shops/export"]').first().click();
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
expect(download.suggestedFilename()).toContain('.csv');
|
expect(download.suggestedFilename()).toContain('.csv');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module.exports = defineConfig({
|
|||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
|
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:8045',
|
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8045',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
locale: 'ko-KR',
|
locale: 'ko-KR',
|
||||||
|
|||||||
@@ -29,16 +29,25 @@ CREATE TABLE IF NOT EXISTS `designated_shop` (
|
|||||||
`ds_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '상호명',
|
`ds_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '상호명',
|
||||||
`ds_biz_no` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '사업자번호',
|
`ds_biz_no` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '사업자번호',
|
||||||
`ds_rep_name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '대표자명',
|
`ds_rep_name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '대표자명',
|
||||||
`ds_va_number` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '고정 가상계좌 번호',
|
`ds_biz_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '업태',
|
||||||
|
`ds_biz_kind` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '업종',
|
||||||
|
`ds_va_number` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '가상계좌(표시용 번호, 계좌번호와 동기화 가능)',
|
||||||
|
`ds_va_bank` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '가상계좌(은행)',
|
||||||
|
`ds_va_account` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '계좌번호',
|
||||||
`ds_zip` VARCHAR(10) NOT NULL DEFAULT '' COMMENT '우편번호',
|
`ds_zip` VARCHAR(10) NOT NULL DEFAULT '' COMMENT '우편번호',
|
||||||
`ds_addr` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '도로명주소',
|
`ds_addr` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '도로명주소',
|
||||||
`ds_addr_jibun` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '지번주소',
|
`ds_addr_jibun` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '지번주소',
|
||||||
|
`ds_addr_detail` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '상세주소(동·호 등)',
|
||||||
`ds_tel` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '일반전화',
|
`ds_tel` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '일반전화',
|
||||||
`ds_rep_phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '개인전화',
|
`ds_rep_phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '개인전화',
|
||||||
`ds_email` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '이메일',
|
`ds_email` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '이메일',
|
||||||
`ds_gugun_code` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '구코드',
|
`ds_gugun_code` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '구코드',
|
||||||
|
`ds_zone_code` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '구역',
|
||||||
|
`ds_branch_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '종사업장번호',
|
||||||
`ds_designated_at` DATE NULL DEFAULT NULL COMMENT '지정일자',
|
`ds_designated_at` DATE NULL DEFAULT NULL COMMENT '지정일자',
|
||||||
`ds_state` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1=정상, 2=폐업, 3=직권해지',
|
`ds_state` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1=정상, 2=폐업, 3=직권해지',
|
||||||
|
`ds_state_changed_at` DATE NULL DEFAULT NULL COMMENT '변경일자',
|
||||||
|
`ds_change_reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '변경사유',
|
||||||
`ds_regdate` DATETIME NOT NULL COMMENT '등록일시',
|
`ds_regdate` DATETIME NOT NULL COMMENT '등록일시',
|
||||||
PRIMARY KEY (`ds_idx`),
|
PRIMARY KEY (`ds_idx`),
|
||||||
KEY `idx_ds_lg_idx` (`ds_lg_idx`),
|
KEY `idx_ds_lg_idx` (`ds_lg_idx`),
|
||||||
|
|||||||
5
writable/database/designated_shop_addr_detail.sql
Normal file
5
writable/database/designated_shop_addr_detail.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- 지정판매소 상세주소(건물명·동·호 등) — 주소 검색으로 채운 도로명/지번과 별도 입력
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `designated_shop`
|
||||||
|
ADD COLUMN `ds_addr_detail` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '상세주소(동·호 등)' AFTER `ds_addr_jibun`;
|
||||||
108
writable/database/designated_shop_ensure_app_columns.sql
Normal file
108
writable/database/designated_shop_ensure_app_columns.sql
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
-- 지정판매소: 앱(DesignatedShopModel / Admin\DesignatedShop)이 기대하는 컬럼을
|
||||||
|
-- 없을 때만 추가합니다. 기존 DB를 login_tables.sql 최신 정의와 맞출 때 사용.
|
||||||
|
-- 실행 예: mysql -h 127.0.0.1 -u USER -p DBNAME < writable/database/designated_shop_ensure_app_columns.sql
|
||||||
|
--
|
||||||
|
-- kr_address 등 외부 테이블 불필요. INFORMATION_SCHEMA 로 존재 여부만 확인합니다.
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
SET @db = DATABASE();
|
||||||
|
|
||||||
|
-- ds_biz_type
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_biz_type') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_biz_type` VARCHAR(100) NOT NULL DEFAULT '''' COMMENT ''업태'' AFTER `ds_rep_name`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_biz_kind
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_biz_kind') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_biz_kind` VARCHAR(100) NOT NULL DEFAULT '''' COMMENT ''업종'' AFTER `ds_biz_type`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_va_bank (ds_va_number 뒤 — 없으면 ds_biz_kind 뒤에 붙임)
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_va_bank') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_va_number') > 0,
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_va_bank` VARCHAR(80) NOT NULL DEFAULT '''' COMMENT ''가상계좌(은행)'' AFTER `ds_va_number`',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_va_bank` VARCHAR(80) NOT NULL DEFAULT '''' COMMENT ''가상계좌(은행)'' AFTER `ds_biz_kind`'
|
||||||
|
)
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_va_account
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_va_account') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_va_account` VARCHAR(50) NOT NULL DEFAULT '''' COMMENT ''계좌번호'' AFTER `ds_va_bank`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_addr_detail
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_addr_detail') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_addr_detail` VARCHAR(200) NOT NULL DEFAULT '''' COMMENT ''상세주소(동·호 등)'' AFTER `ds_addr_jibun`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_zone_code
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_zone_code') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_zone_code` VARCHAR(80) NOT NULL DEFAULT '''' COMMENT ''구역'' AFTER `ds_gugun_code`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_branch_no
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_branch_no') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_branch_no` VARCHAR(50) NOT NULL DEFAULT '''' COMMENT ''종사업장번호'' AFTER `ds_zone_code`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_state_changed_at
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_state_changed_at') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_state_changed_at` DATE NULL DEFAULT NULL COMMENT ''변경일자'' AFTER `ds_state`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_change_reason
|
||||||
|
SET @s = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_change_reason') > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_change_reason` VARCHAR(500) NOT NULL DEFAULT '''' COMMENT ''변경사유'' AFTER `ds_state_changed_at`'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ds_va_number 뒤에 va_bank를 넣었을 수 있음 — 구 스키마에 ds_designated_at 등만 있는 경우
|
||||||
|
UPDATE `designated_shop`
|
||||||
|
SET `ds_va_account` = `ds_va_number`
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_va_account'
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_va_number'
|
||||||
|
)
|
||||||
|
AND (`ds_va_account` = '' OR `ds_va_account` IS NULL)
|
||||||
|
AND `ds_va_number` IS NOT NULL
|
||||||
|
AND `ds_va_number` != '';
|
||||||
25
writable/database/designated_shop_extended_columns.sql
Normal file
25
writable/database/designated_shop_extended_columns.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- 지정판매소 확장 컬럼 (업태·업종·구역·종사업장·가상계좌 은행/계좌·변경일자·변경사유)
|
||||||
|
-- 기존 DB: mysql ... < writable/database/designated_shop_extended_columns.sql
|
||||||
|
-- 컬럼이 이미 있으면 수동으로 스킵하거나 에러 무시 후 진행
|
||||||
|
--
|
||||||
|
-- 권장: 컬럼 유무를 자동 판별하려면 대신
|
||||||
|
-- writable/database/designated_shop_ensure_app_columns.sql
|
||||||
|
-- 를 실행하세요(여러 번 실행해도 안전).
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `designated_shop`
|
||||||
|
ADD COLUMN `ds_biz_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '업태' AFTER `ds_rep_name`,
|
||||||
|
ADD COLUMN `ds_biz_kind` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '업종' AFTER `ds_biz_type`,
|
||||||
|
ADD COLUMN `ds_zone_code` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '구역' AFTER `ds_gugun_code`,
|
||||||
|
ADD COLUMN `ds_branch_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '종사업장번호' AFTER `ds_zone_code`,
|
||||||
|
ADD COLUMN `ds_va_bank` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '가상계좌(은행)' AFTER `ds_va_number`,
|
||||||
|
ADD COLUMN `ds_va_account` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '계좌번호' AFTER `ds_va_bank`,
|
||||||
|
ADD COLUMN `ds_state_changed_at` DATE NULL DEFAULT NULL COMMENT '변경일자' AFTER `ds_state`,
|
||||||
|
ADD COLUMN `ds_change_reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '변경사유' AFTER `ds_state_changed_at`;
|
||||||
|
|
||||||
|
UPDATE `designated_shop`
|
||||||
|
SET `ds_va_account` = `ds_va_number`
|
||||||
|
WHERE (`ds_va_account` = '' OR `ds_va_account` IS NULL)
|
||||||
|
AND `ds_va_number` IS NOT NULL
|
||||||
|
AND `ds_va_number` != '';
|
||||||
@@ -106,16 +106,25 @@ CREATE TABLE IF NOT EXISTS `designated_shop` (
|
|||||||
`ds_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '상호명',
|
`ds_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '상호명',
|
||||||
`ds_biz_no` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '사업자번호',
|
`ds_biz_no` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '사업자번호',
|
||||||
`ds_rep_name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '대표자명',
|
`ds_rep_name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '대표자명',
|
||||||
`ds_va_number` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '고정 가상계좌 번호',
|
`ds_biz_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '업태',
|
||||||
|
`ds_biz_kind` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '업종',
|
||||||
|
`ds_va_number` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '가상계좌(표시용 번호, 계좌번호와 동기화 가능)',
|
||||||
|
`ds_va_bank` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '가상계좌(은행)',
|
||||||
|
`ds_va_account` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '계좌번호',
|
||||||
`ds_zip` VARCHAR(10) NOT NULL DEFAULT '' COMMENT '우편번호',
|
`ds_zip` VARCHAR(10) NOT NULL DEFAULT '' COMMENT '우편번호',
|
||||||
`ds_addr` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '도로명주소',
|
`ds_addr` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '도로명주소',
|
||||||
`ds_addr_jibun` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '지번주소',
|
`ds_addr_jibun` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '지번주소',
|
||||||
|
`ds_addr_detail` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '상세주소(동·호 등)',
|
||||||
`ds_tel` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '일반전화',
|
`ds_tel` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '일반전화',
|
||||||
`ds_rep_phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '개인전화',
|
`ds_rep_phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '개인전화',
|
||||||
`ds_email` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '이메일',
|
`ds_email` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '이메일',
|
||||||
`ds_gugun_code` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '구코드',
|
`ds_gugun_code` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '구코드',
|
||||||
|
`ds_zone_code` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '구역',
|
||||||
|
`ds_branch_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '종사업장번호',
|
||||||
`ds_designated_at` DATE NULL DEFAULT NULL COMMENT '지정일자',
|
`ds_designated_at` DATE NULL DEFAULT NULL COMMENT '지정일자',
|
||||||
`ds_state` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1=정상, 2=폐업, 3=직권해지',
|
`ds_state` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1=정상, 2=폐업, 3=직권해지',
|
||||||
|
`ds_state_changed_at` DATE NULL DEFAULT NULL COMMENT '변경일자',
|
||||||
|
`ds_change_reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '변경사유',
|
||||||
`ds_regdate` DATETIME NOT NULL COMMENT '등록일시',
|
`ds_regdate` DATETIME NOT NULL COMMENT '등록일시',
|
||||||
PRIMARY KEY (`ds_idx`),
|
PRIMARY KEY (`ds_idx`),
|
||||||
KEY `idx_ds_lg_idx` (`ds_lg_idx`),
|
KEY `idx_ds_lg_idx` (`ds_lg_idx`),
|
||||||
|
|||||||
7
writable/database/menu_link_designated_shop_barcode.sql
Normal file
7
writable/database/menu_link_designated_shop_barcode.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- 기존 DB: '지정판매소 바코드 출력' 메뉴를 전용 URL로 변경
|
||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
UPDATE `menu` m
|
||||||
|
INNER JOIN `menu_type` t ON t.mt_idx = m.mt_idx AND t.mt_code = 'site'
|
||||||
|
SET m.mm_link = 'bag/designated-shops/barcode'
|
||||||
|
WHERE m.mm_name = '지정판매소 바코드 출력';
|
||||||
10
writable/database/menu_link_district_new_cancel.sql
Normal file
10
writable/database/menu_link_district_new_cancel.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- 기존 DB: 지정 판매소 신규/취소 현황 메뉴를 GBMS형 전용 URL로 변경
|
||||||
|
-- UTF-8: mysql --default-character-set=utf8mb4 ...
|
||||||
|
|
||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
UPDATE `menu` m
|
||||||
|
INNER JOIN `menu_type` t ON t.mt_idx = m.mt_idx AND t.mt_code = 'site'
|
||||||
|
SET m.mm_link = 'bag/designated-shops/district-new-cancel'
|
||||||
|
WHERE m.mm_name IN ('지정 판매소 신규/취소 현황', '지정 판매소 현황')
|
||||||
|
AND m.mm_link IN ('bag/designated-shops/status', '');
|
||||||
@@ -19,12 +19,12 @@ SET m.mm_link = CASE m.mm_name
|
|||||||
WHEN '업체 관리' THEN 'bag/companies'
|
WHEN '업체 관리' THEN 'bag/companies'
|
||||||
WHEN '무료용 대상자 관리' THEN 'bag/free-recipients'
|
WHEN '무료용 대상자 관리' THEN 'bag/free-recipients'
|
||||||
WHEN '지정 판매소 관리' THEN 'bag/designated-shops'
|
WHEN '지정 판매소 관리' THEN 'bag/designated-shops'
|
||||||
WHEN '지정 판매소 조회' THEN 'bag/designated-shops'
|
WHEN '지정 판매소 조회' THEN 'bag/designated-shops/browse'
|
||||||
WHEN '지정 판매소 신규/취소 현황' THEN 'bag/designated-shops/status'
|
WHEN '지정 판매소 신규/취소 현황' THEN 'bag/designated-shops/district-new-cancel'
|
||||||
WHEN '지정판매소 바코드 출력' THEN 'bag/designated-shops'
|
WHEN '지정판매소 바코드 출력' THEN 'bag/designated-shops/barcode'
|
||||||
WHEN 'PASSWORD 변경' THEN 'bag/password-change'
|
WHEN 'PASSWORD 변경' THEN 'bag/password-change'
|
||||||
WHEN '환경 설정' THEN 'dashboard'
|
WHEN '환경 설정' THEN 'dashboard'
|
||||||
WHEN '지정 판매소 현황' THEN 'bag/designated-shops/status'
|
WHEN '지정 판매소 현황' THEN 'bag/designated-shops/district-new-cancel'
|
||||||
WHEN '발주 등록' THEN 'bag/order/create'
|
WHEN '발주 등록' THEN 'bag/order/create'
|
||||||
WHEN '발주 변경' THEN 'bag/bag-orders'
|
WHEN '발주 변경' THEN 'bag/bag-orders'
|
||||||
WHEN '발주 현황' THEN 'bag/bag-orders'
|
WHEN '발주 현황' THEN 'bag/bag-orders'
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ SELECT @mt_site, 1, t.mm_name,
|
|||||||
WHEN '업체 관리' THEN 'bag/companies'
|
WHEN '업체 관리' THEN 'bag/companies'
|
||||||
WHEN '무료용 대상자 관리' THEN 'bag/free-recipients'
|
WHEN '무료용 대상자 관리' THEN 'bag/free-recipients'
|
||||||
WHEN '지정 판매소 관리' THEN 'bag/designated-shops'
|
WHEN '지정 판매소 관리' THEN 'bag/designated-shops'
|
||||||
WHEN '지정 판매소 조회' THEN 'bag/designated-shops'
|
WHEN '지정 판매소 조회' THEN 'bag/designated-shops/browse'
|
||||||
WHEN '지정 판매소 신규/취소 현황' THEN 'bag/designated-shops/status'
|
WHEN '지정 판매소 신규/취소 현황' THEN 'bag/designated-shops/district-new-cancel'
|
||||||
WHEN '지정판매소 바코드 출력' THEN 'bag/designated-shops'
|
WHEN '지정판매소 바코드 출력' THEN 'bag/designated-shops/barcode'
|
||||||
WHEN 'PASSWORD 변경' THEN 'bag/password-change'
|
WHEN 'PASSWORD 변경' THEN 'bag/password-change'
|
||||||
WHEN '환경 설정' THEN 'dashboard'
|
WHEN '환경 설정' THEN 'dashboard'
|
||||||
WHEN '지정 판매소 현황' THEN 'bag/designated-shops/status'
|
WHEN '지정 판매소 현황' THEN 'bag/designated-shops/district-new-cancel'
|
||||||
ELSE ''
|
ELSE ''
|
||||||
END,
|
END,
|
||||||
@parent_basic, 1, t.mm_num, 0, '', 'Y'
|
@parent_basic, 1, t.mm_num, 0, '', 'Y'
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
-- 지정판매소 신규/취소 현황(연도별) 테스트 데이터
|
||||||
|
-- 목적: district-new-cancel/status 화면에서 2022~2025 연도 전환 테스트
|
||||||
|
-- 기본값: ds_lg_idx=1, ds_gugun_code=110209(북구). 환경에 맞게 값 변경 후 실행하세요.
|
||||||
|
-- 실행 예:
|
||||||
|
-- mysql -h 127.0.0.1 -u jongryangje -p jongryangje_dev < writable/database/seed_designated_shops_status_multi_years.sql
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
-- 재실행 가능하도록 테스트 prefix 데이터만 정리
|
||||||
|
DELETE FROM `designated_shop` WHERE `ds_shop_no` LIKE 'ZZSTAT-%';
|
||||||
|
|
||||||
|
INSERT INTO `designated_shop` (
|
||||||
|
`ds_lg_idx`, `ds_mb_idx`, `ds_shop_no`, `ds_name`, `ds_biz_no`, `ds_rep_name`,
|
||||||
|
`ds_va_number`, `ds_zip`, `ds_addr`, `ds_addr_jibun`, `ds_addr_detail`,
|
||||||
|
`ds_tel`, `ds_rep_phone`, `ds_email`,
|
||||||
|
`ds_gugun_code`, `ds_zone_code`,
|
||||||
|
`ds_designated_at`, `ds_state`, `ds_state_changed_at`, `ds_change_reason`, `ds_regdate`
|
||||||
|
) VALUES
|
||||||
|
-- 2022 지정 2건
|
||||||
|
(1, NULL, 'ZZSTAT-2201', '현황테스트 A', '901-22-00001', '홍길동', '', '41590', '대구광역시 북구 테스트로 2201', '대구 북구 테스트동 2201', '101호', '053-220-0001', '01022000001', 'zzstat2201@test.local', '110209', '북구-A', '2022-03-01', 1, NULL, '', '2022-03-01 09:00:00'),
|
||||||
|
(1, NULL, 'ZZSTAT-2202', '현황테스트 J', '901-22-00002', '김현황', '', '41590', '대구광역시 북구 테스트로 2202', '대구 북구 테스트동 2202', '102호', '053-220-0002', '01022000002', 'zzstat2202@test.local', '110209', '북구-A', '2022-08-08', 2, '2023-12-20', '테스트 취소(2023)', '2022-08-08 09:00:00'),
|
||||||
|
|
||||||
|
-- 2023 지정 2건 (이 중 1건은 2024에 취소)
|
||||||
|
(1, NULL, 'ZZSTAT-2301', '현황테스트 B', '901-23-00001', '이현황', '', '41590', '대구광역시 북구 테스트로 2301', '대구 북구 테스트동 2301', '201호', '053-230-0001', '01023000001', 'zzstat2301@test.local', '110209', '북구-B', '2023-04-10', 1, NULL, '', '2023-04-10 09:00:00'),
|
||||||
|
(1, NULL, 'ZZSTAT-2302', '현황테스트 C', '901-23-00002', '박현황', '', '41590', '대구광역시 북구 테스트로 2302', '대구 북구 테스트동 2302', '202호', '053-230-0002', '01023000002', 'zzstat2302@test.local', '110209', '북구-B', '2023-07-01', 2, '2024-05-02', '테스트 취소(2024)', '2023-07-01 09:00:00'),
|
||||||
|
|
||||||
|
-- 2024 지정 4건 (이 중 1건은 2024에 취소)
|
||||||
|
(1, NULL, 'ZZSTAT-2401', '현황테스트 D', '901-24-00001', '최현황', '', '41590', '대구광역시 북구 테스트로 2401', '대구 북구 테스트동 2401', '301호', '053-240-0001', '01024000001', 'zzstat2401@test.local', '110209', '북구-C', '2024-01-15', 1, NULL, '', '2024-01-15 09:00:00'),
|
||||||
|
(1, NULL, 'ZZSTAT-2402', '현황테스트 E', '901-24-00002', '정현황', '', '41590', '대구광역시 북구 테스트로 2402', '대구 북구 테스트동 2402', '302호', '053-240-0002', '01024000002', 'zzstat2402@test.local', '110209', '북구-C', '2024-06-20', 1, NULL, '', '2024-06-20 09:00:00'),
|
||||||
|
(1, NULL, 'ZZSTAT-2403', '현황테스트 F', '901-24-00003', '강현황', '', '41590', '대구광역시 북구 테스트로 2403', '대구 북구 테스트동 2403', '303호', '053-240-0003', '01024000003', 'zzstat2403@test.local', '110209', '북구-D', '2024-09-01', 3, '2024-12-01', '테스트 해지(2024)', '2024-09-01 09:00:00'),
|
||||||
|
(1, NULL, 'ZZSTAT-2404', '현황테스트 G', '901-24-00004', '신현황', '', '41590', '대구광역시 북구 테스트로 2404', '대구 북구 테스트동 2404', '304호', '053-240-0004', '01024000004', 'zzstat2404@test.local', '110209', '북구-D', '2024-11-12', 1, NULL, '', '2024-11-12 09:00:00'),
|
||||||
|
|
||||||
|
-- 2025 지정 2건 (이 중 1건은 2025에 취소)
|
||||||
|
(1, NULL, 'ZZSTAT-2501', '현황테스트 H', '901-25-00001', '오현황', '', '41590', '대구광역시 북구 테스트로 2501', '대구 북구 테스트동 2501', '401호', '053-250-0001', '01025000001', 'zzstat2501@test.local', '110209', '북구-E', '2025-02-01', 1, NULL, '', '2025-02-01 09:00:00'),
|
||||||
|
(1, NULL, 'ZZSTAT-2502', '현황테스트 I', '901-25-00002', '유현황', '', '41590', '대구광역시 북구 테스트로 2502', '대구 북구 테스트동 2502', '402호', '053-250-0002', '01025000002', 'zzstat2502@test.local', '110209', '북구-E', '2025-03-20', 2, '2025-10-15', '테스트 취소(2025)', '2025-03-20 09:00:00');
|
||||||
|
|
||||||
|
-- 참고: 추가된 "지정" 건수(연도별)
|
||||||
|
-- 2022: 2건 / 2023: 2건 / 2024: 4건 / 2025: 2건
|
||||||
|
|
||||||
|
-- 검증 1) prefix 데이터의 연도별 지정/취소 건수
|
||||||
|
-- SELECT
|
||||||
|
-- YEAR(ds_designated_at) AS yr,
|
||||||
|
-- COUNT(*) AS designated_cnt,
|
||||||
|
-- SUM(CASE WHEN ds_state IN (2,3) AND ds_state_changed_at IS NOT NULL THEN YEAR(ds_state_changed_at)=YEAR(ds_designated_at) ELSE 0 END) AS same_year_cancel_cnt
|
||||||
|
-- FROM designated_shop
|
||||||
|
-- WHERE ds_shop_no LIKE 'ZZSTAT-%'
|
||||||
|
-- GROUP BY YEAR(ds_designated_at)
|
||||||
|
-- ORDER BY yr;
|
||||||
|
|
||||||
|
-- 검증 2) 2024 현황 기대값(prefix 데이터만)
|
||||||
|
-- 종전(2023말)=3, 지정(2024)=4, 취소(2024)=2, 현행(2024말)=5
|
||||||
|
-- SELECT
|
||||||
|
-- SUM(CASE
|
||||||
|
-- WHEN COALESCE(ds_designated_at, DATE(ds_regdate)) <= '2023-12-31'
|
||||||
|
-- AND (ds_state = 1 OR (ds_state IN (2,3) AND COALESCE(ds_state_changed_at, DATE(ds_regdate)) > '2023-12-31'))
|
||||||
|
-- THEN 1 ELSE 0 END) AS prev_end_2023,
|
||||||
|
-- SUM(CASE WHEN YEAR(COALESCE(ds_designated_at, DATE(ds_regdate))) = 2024 THEN 1 ELSE 0 END) AS designated_2024,
|
||||||
|
-- SUM(CASE WHEN ds_state IN (2,3) AND YEAR(COALESCE(ds_state_changed_at, DATE(ds_regdate))) = 2024 THEN 1 ELSE 0 END) AS cancelled_2024,
|
||||||
|
-- SUM(CASE
|
||||||
|
-- WHEN COALESCE(ds_designated_at, DATE(ds_regdate)) <= '2024-12-31'
|
||||||
|
-- AND (ds_state = 1 OR (ds_state IN (2,3) AND COALESCE(ds_state_changed_at, DATE(ds_regdate)) > '2024-12-31'))
|
||||||
|
-- THEN 1 ELSE 0 END) AS curr_end_2024
|
||||||
|
-- FROM designated_shop
|
||||||
|
-- WHERE ds_shop_no LIKE 'ZZSTAT-%';
|
||||||
43
writable/database/seed_designated_shops_test_30.sql
Normal file
43
writable/database/seed_designated_shops_test_30.sql
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
-- 테스트 지정판매소 30건 (동일 스크립트 재실행 시 기존 ZZTEST 행 삭제 후 삽입)
|
||||||
|
-- 기본: ds_lg_idx = 1 (북구청), ds_gugun_code = 110209 — 환경에 맞게 아래 DELETE/INSERT의 1, 110209만 조정하세요.
|
||||||
|
-- 실행: mysql -h 127.0.0.1 -u jongryangje -p jongryangje_dev < writable/database/seed_designated_shops_test_30.sql
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
DELETE FROM `designated_shop` WHERE `ds_shop_no` LIKE 'ZZTEST-%';
|
||||||
|
|
||||||
|
INSERT INTO `designated_shop` (
|
||||||
|
`ds_lg_idx`, `ds_mb_idx`, `ds_shop_no`, `ds_name`, `ds_biz_no`, `ds_rep_name`,
|
||||||
|
`ds_va_number`, `ds_zip`, `ds_addr`, `ds_addr_jibun`, `ds_tel`, `ds_rep_phone`, `ds_email`,
|
||||||
|
`ds_gugun_code`, `ds_designated_at`, `ds_state`, `ds_regdate`
|
||||||
|
) VALUES
|
||||||
|
(1, NULL, 'ZZTEST-00001', '테스트편의점 01', '784-12-00001', '김테01', '', '41590', '대구광역시 북구 테스트로 1', '대구 북구 테스트동 1', '053-000-0001', '01010000001', 'seed01@test.local', '110209', '2026-01-01', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00002', '테스트편의점 02', '784-12-00002', '김테02', '', '41590', '대구광역시 북구 테스트로 2', '대구 북구 테스트동 2', '053-000-0002', '01010000002', 'seed02@test.local', '110209', '2026-01-02', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00003', '테스트편의점 03', '784-12-00003', '김테03', '', '41590', '대구광역시 북구 테스트로 3', '대구 북구 테스트동 3', '053-000-0003', '01010000003', 'seed03@test.local', '110209', '2026-01-03', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00004', '테스트편의점 04', '784-12-00004', '김테04', '', '41590', '대구광역시 북구 테스트로 4', '대구 북구 테스트동 4', '053-000-0004', '01010000004', 'seed04@test.local', '110209', '2026-01-04', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00005', '테스트편의점 05', '784-12-00005', '김테05', '', '41590', '대구광역시 북구 테스트로 5', '대구 북구 테스트동 5', '053-000-0005', '01010000005', 'seed05@test.local', '110209', '2026-01-05', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00006', '테스트편의점 06', '784-12-00006', '김테06', '', '41590', '대구광역시 북구 테스트로 6', '대구 북구 테스트동 6', '053-000-0006', '01010000006', 'seed06@test.local', '110209', '2026-01-06', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00007', '테스트편의점 07', '784-12-00007', '김테07', '', '41590', '대구광역시 북구 테스트로 7', '대구 북구 테스트동 7', '053-000-0007', '01010000007', 'seed07@test.local', '110209', '2026-01-07', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00008', '테스트편의점 08', '784-12-00008', '김테08', '', '41590', '대구광역시 북구 테스트로 8', '대구 북구 테스트동 8', '053-000-0008', '01010000008', 'seed08@test.local', '110209', '2026-01-08', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00009', '테스트편의점 09', '784-12-00009', '김테09', '', '41590', '대구광역시 북구 테스트로 9', '대구 북구 테스트동 9', '053-000-0009', '01010000009', 'seed09@test.local', '110209', '2026-01-09', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00010', '테스트편의점 10', '784-12-00010', '김테10', '', '41590', '대구광역시 북구 테스트로 10', '대구 북구 테스트동 10', '053-000-0010', '01010000010', 'seed10@test.local', '110209', '2026-01-10', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00011', '테스트마트 11', '784-12-00011', '이테11', '', '41590', '대구광역시 북구 테스트로 11', '대구 북구 테스트동 11', '053-000-0011', '01010000011', 'seed11@test.local', '110209', '2026-01-11', 2, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00012', '테스트마트 12', '784-12-00012', '이테12', '', '41590', '대구광역시 북구 테스트로 12', '대구 북구 테스트동 12', '053-000-0012', '01010000012', 'seed12@test.local', '110209', '2026-01-12', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00013', '테스트마트 13', '784-12-00013', '이테13', '', '41590', '대구광역시 북구 테스트로 13', '대구 북구 테스트동 13', '053-000-0013', '01010000013', 'seed13@test.local', '110209', '2026-01-13', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00014', '테스트마트 14', '784-12-00014', '이테14', '', '41590', '대구광역시 북구 테스트로 14', '대구 북구 테스트동 14', '053-000-0014', '01010000014', 'seed14@test.local', '110209', '2026-01-14', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00015', '테스트마트 15', '784-12-00015', '이테15', '', '41590', '대구광역시 북구 테스트로 15', '대구 북구 테스트동 15', '053-000-0015', '01010000015', 'seed15@test.local', '110209', '2026-01-15', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00016', '테스트슈퍼 16', '784-12-00016', '박테16', '', '41590', '대구광역시 북구 테스트로 16', '대구 북구 테스트동 16', '053-000-0016', '01010000016', 'seed16@test.local', '110209', '2026-01-16', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00017', '테스트슈퍼 17', '784-12-00017', '박테17', '', '41590', '대구광역시 북구 테스트로 17', '대구 북구 테스트동 17', '053-000-0017', '01010000017', 'seed17@test.local', '110209', '2026-01-17', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00018', '테스트슈퍼 18', '784-12-00018', '박테18', '', '41590', '대구광역시 북구 테스트로 18', '대구 북구 테스트동 18', '053-000-0018', '01010000018', 'seed18@test.local', '110209', '2026-01-18', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00019', '테스트슈퍼 19', '784-12-00019', '박테19', '', '41590', '대구광역시 북구 테스트로 19', '대구 북구 테스트동 19', '053-000-0019', '01010000019', 'seed19@test.local', '110209', '2026-01-19', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00020', '테스트슈퍼 20', '784-12-00020', '박테20', '', '41590', '대구광역시 북구 테스트로 20', '대구 북구 테스트동 20', '053-000-0020', '01010000020', 'seed20@test.local', '110209', '2026-01-20', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00021', '테스트상회 21', '784-12-00021', '최테21', '', '41590', '대구광역시 북구 테스트로 21', '대구 북구 테스트동 21', '053-000-0021', '01010000021', 'seed21@test.local', '110209', '2026-01-21', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00022', '테스트상회 22', '784-12-00022', '최테22', '', '41590', '대구광역시 북구 테스트로 22', '대구 북구 테스트동 22', '053-000-0022', '01010000022', 'seed22@test.local', '110209', '2026-01-22', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00023', '테스트상회 23', '784-12-00023', '최테23', '', '41590', '대구광역시 북구 테스트로 23', '대구 북구 테스트동 23', '053-000-0023', '01010000023', 'seed23@test.local', '110209', '2026-01-23', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00024', '테스트상회 24', '784-12-00024', '최테24', '', '41590', '대구광역시 북구 테스트로 24', '대구 북구 테스트동 24', '053-000-0024', '01010000024', 'seed24@test.local', '110209', '2026-01-24', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00025', '테스트상회 25', '784-12-00025', '최테25', '', '41590', '대구광역시 북구 테스트로 25', '대구 북구 테스트동 25', '053-000-0025', '01010000025', 'seed25@test.local', '110209', '2026-01-25', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00026', '테스트복합 26', '784-12-00026', '정테26', '', '41590', '대구광역시 북구 테스트로 26', '대구 북구 테스트동 26', '053-000-0026', '01010000026', 'seed26@test.local', '110209', '2026-01-26', 3, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00027', '테스트복합 27', '784-12-00027', '정테27', '', '41590', '대구광역시 북구 테스트로 27', '대구 북구 테스트동 27', '053-000-0027', '01010000027', 'seed27@test.local', '110209', '2026-01-27', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00028', '테스트복합 28', '784-12-00028', '정테28', '', '41590', '대구광역시 북구 테스트로 28', '대구 북구 테스트동 28', '053-000-0028', '01010000028', 'seed28@test.local', '110209', '2026-01-28', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00029', '테스트복합 29', '784-12-00029', '정테29', '', '41590', '대구광역시 북구 테스트로 29', '대구 북구 테스트동 29', '053-000-0029', '01010000029', 'seed29@test.local', '110209', '2026-01-29', 1, NOW()),
|
||||||
|
(1, NULL, 'ZZTEST-00030', '테스트복합 30', '784-12-00030', '정테30', '', '41590', '대구광역시 북구 테스트로 30', '대구 북구 테스트동 30', '053-000-0030', '01010000030', 'seed30@test.local', '110209', '2026-01-30', 1, NOW());
|
||||||
56
writable/database/seed_tester_accounts_trash_host.sql
Normal file
56
writable/database/seed_tester_accounts_trash_host.sql
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
-- 테스터 계정 (비밀번호: test1234!) — 관리자 회원 등록과 동일 필드 구성
|
||||||
|
-- 비밀번호 해시: PHP password_hash('test1234!', PASSWORD_DEFAULT)
|
||||||
|
-- tester_local → local_government 중구청 (lg_idx=10, lg_code=110201)
|
||||||
|
-- 실행 예: mysql -h 116.122.157.166 -P 3306 -u jongryangje -p jongryangje_dev < writable/database/seed_tester_accounts_trash_host.sql
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET @pw := '$2y$10$D.rk9Dtce7qitSCaPO0W2.DROcEwpe3otxE.QF0qWPb63bCBhtE5u';
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
DELETE mar FROM member_approval_request mar
|
||||||
|
INNER JOIN member m ON m.mb_idx = mar.mb_idx
|
||||||
|
WHERE m.mb_id IN ('tester_badmin', 'tester_admin', 'tester_local', 'tester_shop', 'tester_user');
|
||||||
|
|
||||||
|
INSERT INTO `member` (
|
||||||
|
`mb_id`, `mb_passwd`, `mb_totp_secret`, `mb_totp_enabled`,
|
||||||
|
`mb_name`, `mb_email`, `mb_phone`, `mb_lang`,
|
||||||
|
`mb_level`, `mb_group`, `mb_lg_idx`, `mb_state`, `mb_regdate`
|
||||||
|
) VALUES
|
||||||
|
('tester_badmin', @pw, NULL, 0, '테스터본부', 'tester_badmin@test.com', '010-0000-0005', 'ko', 5, '', NULL, 1, NOW()),
|
||||||
|
('tester_admin', @pw, NULL, 0, '테스터관리자', 'tester_admin@test.com', '010-0000-0001', 'ko', 4, '', NULL, 1, NOW()),
|
||||||
|
('tester_local', @pw, NULL, 0, '테스터지자체(중구)', 'tester_local@test.com', '010-0000-0002', 'ko', 3, '', 10, 1, NOW()),
|
||||||
|
('tester_shop', @pw, NULL, 0, '테스터판매소', 'tester_shop@test.com', '010-0000-0003', 'ko', 2, '', NULL, 1, NOW()),
|
||||||
|
('tester_user', @pw, NULL, 0, '테스터사용자', 'tester_user@test.com', '010-0000-0004', 'ko', 1, '', NULL, 1, NOW())
|
||||||
|
AS new
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`mb_passwd` = new.`mb_passwd`,
|
||||||
|
`mb_totp_secret` = NULL,
|
||||||
|
`mb_totp_enabled` = 0,
|
||||||
|
`mb_name` = new.`mb_name`,
|
||||||
|
`mb_email` = new.`mb_email`,
|
||||||
|
`mb_phone` = new.`mb_phone`,
|
||||||
|
`mb_level` = new.`mb_level`,
|
||||||
|
`mb_group` = new.`mb_group`,
|
||||||
|
`mb_lg_idx` = new.`mb_lg_idx`,
|
||||||
|
`mb_state` = 1;
|
||||||
|
|
||||||
|
INSERT INTO `member_approval_request` (
|
||||||
|
`mb_idx`, `mar_requested_level`, `mar_status`, `mar_request_note`,
|
||||||
|
`mar_reject_reason`, `mar_requested_at`, `mar_requested_by`,
|
||||||
|
`mar_processed_at`, `mar_processed_by`
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.`mb_idx`,
|
||||||
|
m.`mb_level`,
|
||||||
|
'approved',
|
||||||
|
'테스트 계정 시드',
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
m.`mb_idx`,
|
||||||
|
NOW(),
|
||||||
|
m.`mb_idx`
|
||||||
|
FROM `member` m
|
||||||
|
WHERE m.`mb_id` IN ('tester_badmin', 'tester_admin', 'tester_local', 'tester_shop', 'tester_user');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user