feat: 지정판매소 관리 화면 개선 — 좌우 배치·상세 폼·리스트 정렬·동코드 표시
- 리스트(좌)/상세정보(우) 좌우 배치, 상세를 라벨-값 폼 형태로 변경 - 리스트 컬럼(번호·대표자·상호·구군(동코드)·지정일·구역·상태) + 컬럼별 정렬 - 페이징 제거 → 전체 로드 후 패널 내 스크롤 - 주소로 동 기본코드(D) 산출해 저장·표시(ds_dong_code), 등록 시 주소 선택하면 동코드 표시 - 인쇄·엑셀저장을 관리 화면에도 추가(엑셀에 동코드·지정일 포함) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,7 @@ $routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void
|
|||||||
$routes->get('designated-shops/browse', 'Admin\DesignatedShop::browse');
|
$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/resolve-address-codes', 'Admin\DesignatedShop::resolveAddressCodes');
|
||||||
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
|
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
|
||||||
$routes->get('designated-shops/edit/(:num)', 'Admin\DesignatedShop::edit/$1');
|
$routes->get('designated-shops/edit/(:num)', 'Admin\DesignatedShop::edit/$1');
|
||||||
$routes->post('designated-shops/update/(:num)', 'Admin\DesignatedShop::update/$1');
|
$routes->post('designated-shops/update/(:num)', 'Admin\DesignatedShop::update/$1');
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class DesignatedShop extends BaseController
|
|||||||
private DesignatedShopModel $shopModel;
|
private DesignatedShopModel $shopModel;
|
||||||
private LocalGovernmentModel $lgModel;
|
private LocalGovernmentModel $lgModel;
|
||||||
private Roles $roles;
|
private Roles $roles;
|
||||||
|
private ?bool $dongColumnExists = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -223,9 +224,10 @@ class DesignatedShop extends BaseController
|
|||||||
/**
|
/**
|
||||||
* @param list<object> $list
|
* @param list<object> $list
|
||||||
* @param array<int, string> $lgMap
|
* @param array<int, string> $lgMap
|
||||||
|
* @param array<int, array{code: string, name: string}> $dongMap ds_idx => [동코드, 동명]
|
||||||
* @return list<array<string, mixed>>
|
* @return list<array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
private function buildDesignatedShopDetailPayload(array $list, array $lgMap): array
|
private function buildDesignatedShopDetailPayload(array $list, array $lgMap, array $dongMap = []): array
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx() ?? 0;
|
$lgIdx = admin_effective_lg_idx() ?? 0;
|
||||||
@@ -244,6 +246,18 @@ class DesignatedShop extends BaseController
|
|||||||
$stateMap = [1 => '정상', 2 => '폐업', 3 => '직권해지'];
|
$stateMap = [1 => '정상', 2 => '폐업', 3 => '직권해지'];
|
||||||
$da = $row->ds_designated_at ?? null;
|
$da = $row->ds_designated_at ?? null;
|
||||||
$daOut = ($da !== null && $da !== '' && $da !== '0000-00-00') ? (string) $da : '';
|
$daOut = ($da !== null && $da !== '' && $da !== '0000-00-00') ? (string) $da : '';
|
||||||
|
|
||||||
|
$idxKey = (int) ($row->ds_idx ?? 0);
|
||||||
|
$dongCode = $dongMap[$idxKey]['code'] ?? (string) ($row->ds_dong_code ?? '');
|
||||||
|
$dongName = $dongMap[$idxKey]['name'] ?? '';
|
||||||
|
$gugunName = $gugunMap[(string) ($row->ds_gugun_code ?? '')] ?? (string) ($row->ds_gugun_code ?? '');
|
||||||
|
// 구·군 표시값: 동코드가 있으면 "동코드 (동명)", 없으면 기존 구·군명
|
||||||
|
if ($dongCode !== '') {
|
||||||
|
$dongDisplay = $dongCode . ($dongName !== '' ? ' (' . $dongName . ')' : '');
|
||||||
|
} else {
|
||||||
|
$dongDisplay = $gugunName;
|
||||||
|
}
|
||||||
|
|
||||||
$payload[] = [
|
$payload[] = [
|
||||||
'ds_idx' => (int) $row->ds_idx,
|
'ds_idx' => (int) $row->ds_idx,
|
||||||
'ds_shop_no' => $sn,
|
'ds_shop_no' => $sn,
|
||||||
@@ -266,7 +280,10 @@ class DesignatedShop extends BaseController
|
|||||||
'ds_rep_phone' => (string) ($row->ds_rep_phone ?? ''),
|
'ds_rep_phone' => (string) ($row->ds_rep_phone ?? ''),
|
||||||
'ds_email' => (string) ($row->ds_email ?? ''),
|
'ds_email' => (string) ($row->ds_email ?? ''),
|
||||||
'ds_gugun_code' => (string) ($row->ds_gugun_code ?? ''),
|
'ds_gugun_code' => (string) ($row->ds_gugun_code ?? ''),
|
||||||
'gugun_name' => $gugunMap[(string) ($row->ds_gugun_code ?? '')] ?? (string) ($row->ds_gugun_code ?? ''),
|
'gugun_name' => $gugunName,
|
||||||
|
'ds_dong_code' => $dongCode,
|
||||||
|
'dong_name' => $dongName,
|
||||||
|
'dong_display' => $dongDisplay,
|
||||||
'ds_zone_code' => $this->designatedShopScalar($row, 'ds_zone_code'),
|
'ds_zone_code' => $this->designatedShopScalar($row, 'ds_zone_code'),
|
||||||
'ds_branch_no' => $this->designatedShopScalar($row, 'ds_branch_no'),
|
'ds_branch_no' => $this->designatedShopScalar($row, 'ds_branch_no'),
|
||||||
'ds_designated_at' => $daOut,
|
'ds_designated_at' => $daOut,
|
||||||
@@ -300,8 +317,9 @@ class DesignatedShop extends BaseController
|
|||||||
$dsState = $this->request->getGet('ds_state');
|
$dsState = $this->request->getGet('ds_state');
|
||||||
$this->applyDesignatedShopListFilters($this->shopModel, $lgIdx, $dsName, $dsGugunCode, $dsState);
|
$this->applyDesignatedShopListFilters($this->shopModel, $lgIdx, $dsName, $dsGugunCode, $dsState);
|
||||||
|
|
||||||
$list = $this->shopModel->orderBy('ds_idx', 'DESC')->paginate(20);
|
// 페이징 대신 전체 목록을 한 번에 로드 — 리스트 패널 내부 스크롤로 표시
|
||||||
$pager = $this->shopModel->pager;
|
$list = $this->shopModel->orderBy('ds_idx', 'DESC')->findAll();
|
||||||
|
$pager = null;
|
||||||
|
|
||||||
// 지자체 이름 매핑용
|
// 지자체 이름 매핑용
|
||||||
$lgMap = [];
|
$lgMap = [];
|
||||||
@@ -311,7 +329,21 @@ class DesignatedShop extends BaseController
|
|||||||
|
|
||||||
$stateCounts = $this->countDesignatedShopsByState($lgIdx, $dsName, $dsGugunCode, $dsState);
|
$stateCounts = $this->countDesignatedShopsByState($lgIdx, $dsName, $dsGugunCode, $dsState);
|
||||||
$gugunNameMap = $this->gugunCodeNameMap($lgIdx);
|
$gugunNameMap = $this->gugunCodeNameMap($lgIdx);
|
||||||
$detailRows = $this->buildDesignatedShopDetailPayload($list, $lgMap);
|
|
||||||
|
// 각 판매소의 동코드를 주소로 산출 → (컬럼이 있으면) 저장 → 상세 표시에 사용
|
||||||
|
$dongMap = $this->resolveDongCodesForShops($lgIdx, $list);
|
||||||
|
$this->persistDesignatedShopDongCodes($list, $dongMap);
|
||||||
|
$detailRows = $this->buildDesignatedShopDetailPayload($list, $lgMap, $dongMap);
|
||||||
|
|
||||||
|
// 리스트(서버 렌더)에서 동코드 표시용: ds_idx => "동코드 (동명)"
|
||||||
|
$dongDisplayMap = [];
|
||||||
|
foreach ($dongMap as $idx => $d) {
|
||||||
|
$code = (string) ($d['code'] ?? '');
|
||||||
|
$name = (string) ($d['name'] ?? '');
|
||||||
|
if ($code !== '') {
|
||||||
|
$dongDisplayMap[$idx] = $code . ($name !== '' ? ' (' . $name . ')' : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 구군코드 목록 (검색 필터용)
|
// 구군코드 목록 (검색 필터용)
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
@@ -327,6 +359,7 @@ class DesignatedShop extends BaseController
|
|||||||
'gugunCodes' => $gugunCodes,
|
'gugunCodes' => $gugunCodes,
|
||||||
'stateCounts' => $stateCounts,
|
'stateCounts' => $stateCounts,
|
||||||
'gugunNameMap' => $gugunNameMap,
|
'gugunNameMap' => $gugunNameMap,
|
||||||
|
'dongDisplayMap' => $dongDisplayMap,
|
||||||
'detailRowsJson' => json_encode($detailRows, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE),
|
'detailRowsJson' => json_encode($detailRows, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE),
|
||||||
'kakaoJavascriptKey' => $this->kakaoJavascriptKey(),
|
'kakaoJavascriptKey' => $this->kakaoJavascriptKey(),
|
||||||
];
|
];
|
||||||
@@ -477,14 +510,25 @@ class DesignatedShop extends BaseController
|
|||||||
|
|
||||||
$list = $this->shopModel->where('ds_lg_idx', $lgIdx)->orderBy('ds_idx', 'DESC')->findAll();
|
$list = $this->shopModel->where('ds_lg_idx', $lgIdx)->orderBy('ds_idx', 'DESC')->findAll();
|
||||||
|
|
||||||
|
// 동코드(구·군 동코드) 산출
|
||||||
|
$dongMap = $this->resolveDongCodesForShops($lgIdx, $list);
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
foreach ($list as $row) {
|
foreach ($list as $row) {
|
||||||
$stateMap = [1 => '정상', 2 => '폐업', 3 => '직권해지'];
|
$stateMap = [1 => '정상', 2 => '폐업', 3 => '직권해지'];
|
||||||
|
$idxKey = (int) ($row->ds_idx ?? 0);
|
||||||
|
$dCode = (string) ($dongMap[$idxKey]['code'] ?? '');
|
||||||
|
$dName = (string) ($dongMap[$idxKey]['name'] ?? '');
|
||||||
|
$dongDisp = $dCode !== '' ? ($dCode . ($dName !== '' ? ' (' . $dName . ')' : '')) : '';
|
||||||
|
$da = $row->ds_designated_at ?? null;
|
||||||
|
$daDisp = ($da !== null && $da !== '' && (string) $da !== '0000-00-00') ? substr((string) $da, 0, 10) : '';
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
$row->ds_idx,
|
$row->ds_idx,
|
||||||
$row->ds_shop_no,
|
$row->ds_shop_no,
|
||||||
$row->ds_name,
|
$row->ds_name,
|
||||||
$row->ds_rep_name,
|
$row->ds_rep_name,
|
||||||
|
$dongDisp,
|
||||||
|
$daDisp,
|
||||||
$row->ds_biz_no,
|
$row->ds_biz_no,
|
||||||
$this->designatedShopScalar($row, 'ds_biz_type'),
|
$this->designatedShopScalar($row, 'ds_biz_type'),
|
||||||
$this->designatedShopScalar($row, 'ds_biz_kind'),
|
$this->designatedShopScalar($row, 'ds_biz_kind'),
|
||||||
@@ -504,7 +548,7 @@ class DesignatedShop extends BaseController
|
|||||||
export_csv(
|
export_csv(
|
||||||
'지정판매소_' . date('Ymd') . '.csv',
|
'지정판매소_' . date('Ymd') . '.csv',
|
||||||
[
|
[
|
||||||
'번호', '판매소번호', '상호명', '대표자', '사업자번호', '업태', '업종',
|
'번호', '판매소번호', '상호명', '대표자', '구·군(동코드)', '지정일', '사업자번호', '업태', '업종',
|
||||||
'가상계좌은행', '계좌번호', '전화번호', '주소', '구역', '종사업장번호',
|
'가상계좌은행', '계좌번호', '전화번호', '주소', '구역', '종사업장번호',
|
||||||
'변경일자', '변경사유', '상태', '등록일',
|
'변경일자', '변경사유', '상태', '등록일',
|
||||||
],
|
],
|
||||||
@@ -648,6 +692,8 @@ class DesignatedShop extends BaseController
|
|||||||
'ds_branch_no' => (string) $this->request->getPost('ds_branch_no'),
|
'ds_branch_no' => (string) $this->request->getPost('ds_branch_no'),
|
||||||
'ds_designated_at' => $this->request->getPost('ds_designated_at') ?: null,
|
'ds_designated_at' => $this->request->getPost('ds_designated_at') ?: null,
|
||||||
'ds_state' => 1,
|
'ds_state' => 1,
|
||||||
|
// 동코드(컬럼이 있을 때만 저장)
|
||||||
|
...($this->designatedShopHasDongColumn() ? ['ds_dong_code' => (string) ($resolvedNo['dong_code'] ?? '')] : []),
|
||||||
'ds_state_changed_at' => $this->normalizeOptionalDate($this->request->getPost('ds_state_changed_at')),
|
'ds_state_changed_at' => $this->normalizeOptionalDate($this->request->getPost('ds_state_changed_at')),
|
||||||
'ds_change_reason' => (string) $this->request->getPost('ds_change_reason'),
|
'ds_change_reason' => (string) $this->request->getPost('ds_change_reason'),
|
||||||
'ds_regdate' => date('Y-m-d H:i:s'),
|
'ds_regdate' => date('Y-m-d H:i:s'),
|
||||||
@@ -770,6 +816,23 @@ class DesignatedShop extends BaseController
|
|||||||
|
|
||||||
$va = $this->resolveVirtualAccountFromRequest();
|
$va = $this->resolveVirtualAccountFromRequest();
|
||||||
|
|
||||||
|
// 주소로 동코드 재산출(컬럼이 있을 때만 저장). 매칭 실패 시 동코드는 갱신하지 않음.
|
||||||
|
$dongCodeData = [];
|
||||||
|
if ($this->designatedShopHasDongColumn()) {
|
||||||
|
$resolvedDong = $this->resolveDesignatedShopNumberFromAddress(
|
||||||
|
$lgIdx,
|
||||||
|
$addrSido,
|
||||||
|
$addrSigungu,
|
||||||
|
$dsAddr,
|
||||||
|
$dsAddrJibun,
|
||||||
|
$dsZip,
|
||||||
|
$lg
|
||||||
|
);
|
||||||
|
if (($resolvedDong['ok'] ?? false) && ($resolvedDong['dong_code'] ?? '') !== '') {
|
||||||
|
$dongCodeData = ['ds_dong_code' => (string) $resolvedDong['dong_code']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'ds_name' => (string) $this->request->getPost('ds_name'),
|
'ds_name' => (string) $this->request->getPost('ds_name'),
|
||||||
'ds_biz_no' => (string) $this->request->getPost('ds_biz_no'),
|
'ds_biz_no' => (string) $this->request->getPost('ds_biz_no'),
|
||||||
@@ -786,6 +849,7 @@ class DesignatedShop extends BaseController
|
|||||||
'ds_tel' => (string) $this->request->getPost('ds_tel'),
|
'ds_tel' => (string) $this->request->getPost('ds_tel'),
|
||||||
'ds_rep_phone' => (string) $this->request->getPost('ds_rep_phone'),
|
'ds_rep_phone' => (string) $this->request->getPost('ds_rep_phone'),
|
||||||
'ds_email' => (string) $this->request->getPost('ds_email'),
|
'ds_email' => (string) $this->request->getPost('ds_email'),
|
||||||
|
...$dongCodeData,
|
||||||
'ds_zone_code' => (string) $this->request->getPost('ds_zone_code'),
|
'ds_zone_code' => (string) $this->request->getPost('ds_zone_code'),
|
||||||
'ds_branch_no' => (string) $this->request->getPost('ds_branch_no'),
|
'ds_branch_no' => (string) $this->request->getPost('ds_branch_no'),
|
||||||
'ds_designated_at' => $this->request->getPost('ds_designated_at') ?: null,
|
'ds_designated_at' => $this->request->getPost('ds_designated_at') ?: null,
|
||||||
@@ -1532,9 +1596,11 @@ class DesignatedShop extends BaseController
|
|||||||
usort($dCandidates, static fn (array $a, array $b): int => $b['len'] <=> $a['len']);
|
usort($dCandidates, static fn (array $a, array $b): int => $b['len'] <=> $a['len']);
|
||||||
|
|
||||||
$dCode = null;
|
$dCode = null;
|
||||||
|
$dName = '';
|
||||||
foreach ($dCandidates as $cand) {
|
foreach ($dCandidates as $cand) {
|
||||||
if ($this->addressHaystackContainsRegionName($blob, $cand['nm'])) {
|
if ($this->addressHaystackContainsRegionName($blob, $cand['nm'])) {
|
||||||
$dCode = $cand['cd'];
|
$dCode = $cand['cd'];
|
||||||
|
$dName = $cand['nm'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1557,9 +1623,203 @@ class DesignatedShop extends BaseController
|
|||||||
'ok' => true,
|
'ok' => true,
|
||||||
'shop_no' => $prefix . sprintf('%03d', $maxSerial + 1),
|
'shop_no' => $prefix . sprintf('%03d', $maxSerial + 1),
|
||||||
'gugun_code' => $cCode,
|
'gugun_code' => $cCode,
|
||||||
|
'dong_code' => $dCode,
|
||||||
|
'dong_name' => $dName,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [AJAX] 주소 선택 시, 해당 주소의 동 기본코드(D)와 동명을 반환.
|
||||||
|
* 등록 폼에서 주소 검색 직후 「구·군(동코드)」 표시에 사용한다.
|
||||||
|
*/
|
||||||
|
public function resolveAddressCodes()
|
||||||
|
{
|
||||||
|
if (! $this->isSuperAdmin() && ! $this->isLocalAdmin()) {
|
||||||
|
return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => '권한이 없습니다.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = admin_effective_lg_idx();
|
||||||
|
if ($lgIdx === null || $lgIdx <= 0) {
|
||||||
|
return $this->response->setJSON(['ok' => false, 'error' => '작업할 지자체가 선택되지 않았습니다.']);
|
||||||
|
}
|
||||||
|
$lg = $this->lgModel->find($lgIdx);
|
||||||
|
if ($lg === null) {
|
||||||
|
return $this->response->setJSON(['ok' => false, 'error' => '지자체 정보를 찾을 수 없습니다.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$addrSido = (string) $this->request->getPost('addr_search_sido');
|
||||||
|
$addrSigungu = (string) $this->request->getPost('addr_search_sigungu');
|
||||||
|
$dsAddr = (string) $this->request->getPost('ds_addr');
|
||||||
|
$dsAddrJibun = (string) $this->request->getPost('ds_addr_jibun');
|
||||||
|
$dsZip = (string) $this->request->getPost('ds_zip');
|
||||||
|
|
||||||
|
$resolved = $this->resolveDesignatedShopNumberFromAddress(
|
||||||
|
$lgIdx,
|
||||||
|
$addrSido,
|
||||||
|
$addrSigungu,
|
||||||
|
$dsAddr,
|
||||||
|
$dsAddrJibun,
|
||||||
|
$dsZip,
|
||||||
|
$lg
|
||||||
|
);
|
||||||
|
if (! $resolved['ok']) {
|
||||||
|
return $this->response->setJSON(['ok' => false, 'error' => $resolved['error']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'ok' => true,
|
||||||
|
'dong_code' => $resolved['dong_code'] ?? '',
|
||||||
|
'dong_name' => $resolved['dong_name'] ?? '',
|
||||||
|
'gugun_code' => $resolved['gugun_code'] ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* designated_shop 테이블에 ds_dong_code 컬럼이 존재하는지(1회 조회 후 캐시).
|
||||||
|
*/
|
||||||
|
private function designatedShopHasDongColumn(): bool
|
||||||
|
{
|
||||||
|
if ($this->dongColumnExists === null) {
|
||||||
|
$this->dongColumnExists = \Config\Database::connect()->fieldExists('ds_dong_code', 'designated_shop');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dongColumnExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 여러 지정판매소의 동 기본코드(D)를 주소로 일괄 산출한다.
|
||||||
|
* code_detail(B·C·D) 행을 한 번만 로드해 각 판매소를 매칭한다.
|
||||||
|
*
|
||||||
|
* @param list<object> $shops
|
||||||
|
* @return array<int, array{code: string, name: string}> ds_idx => [동코드, 동명]
|
||||||
|
*/
|
||||||
|
private function resolveDongCodesForShops(int $lgIdx, array $shops): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
if ($shops === []) {
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bCk = $this->codeKindIdxByCkCode('B');
|
||||||
|
$cCk = $this->codeKindIdxByCkCode('C');
|
||||||
|
$dCk = $this->codeKindIdxByCkCode('D');
|
||||||
|
if ($bCk === null || $cCk === null || $dCk === null) {
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
$detailModel = model(CodeDetailModel::class);
|
||||||
|
$bRows = $detailModel->getByKind($bCk, true, $lgIdx);
|
||||||
|
$cRows = $detailModel->getByKind($cCk, true, $lgIdx);
|
||||||
|
$dRows = $detailModel->getByKind($dCk, true, $lgIdx);
|
||||||
|
|
||||||
|
foreach ($shops as $row) {
|
||||||
|
$idx = (int) ($row->ds_idx ?? 0);
|
||||||
|
$road = (string) ($row->ds_addr ?? '');
|
||||||
|
$jibun = (string) ($row->ds_addr_jibun ?? '');
|
||||||
|
$zip = (string) ($row->ds_zip ?? '');
|
||||||
|
$blob = trim($road . ' ' . $jibun . ' ' . $zip);
|
||||||
|
if ($blob === '') {
|
||||||
|
$out[$idx] = ['code' => '', 'name' => ''];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B: 시·도
|
||||||
|
$bCode = null;
|
||||||
|
foreach ($bRows as $r) {
|
||||||
|
$nm = trim((string) $r->cd_name);
|
||||||
|
$cd = trim((string) $r->cd_code);
|
||||||
|
if ($nm === '' || $cd === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->koreanRegionTokenMatches($nm, '', $blob)) {
|
||||||
|
$bCode = $cd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($bCode === null || $bCode === '') {
|
||||||
|
$out[$idx] = ['code' => '', 'name' => ''];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// C: 구·군
|
||||||
|
$cCode = null;
|
||||||
|
foreach ($cRows as $r) {
|
||||||
|
$cd = trim((string) $r->cd_code);
|
||||||
|
if ($cd === '' || ! str_starts_with($cd, $bCode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$nm = trim((string) $r->cd_name);
|
||||||
|
if ($nm === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->koreanRegionTokenMatches($nm, '', $blob)) {
|
||||||
|
$cCode = $cd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($cCode === null || $cCode === '') {
|
||||||
|
$out[$idx] = ['code' => '', 'name' => ''];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// D: 동 — 긴 이름 우선 매칭(예: '노원동1가'가 '노원동'보다 먼저)
|
||||||
|
$cands = [];
|
||||||
|
foreach ($dRows as $r) {
|
||||||
|
$cd = trim((string) $r->cd_code);
|
||||||
|
if ($cd === '' || ! str_starts_with($cd, $cCode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$nm = trim((string) $r->cd_name);
|
||||||
|
if ($nm === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$cands[] = ['len' => mb_strlen($nm, 'UTF-8'), 'nm' => $nm, 'cd' => $cd];
|
||||||
|
}
|
||||||
|
usort($cands, static fn (array $a, array $b): int => $b['len'] <=> $a['len']);
|
||||||
|
|
||||||
|
$dCode = '';
|
||||||
|
$dName = '';
|
||||||
|
foreach ($cands as $cand) {
|
||||||
|
if ($this->addressHaystackContainsRegionName($blob, $cand['nm'])) {
|
||||||
|
$dCode = $cand['cd'];
|
||||||
|
$dName = $cand['nm'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out[$idx] = ['code' => $dCode, 'name' => $dName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 산출된 동코드를 ds_dong_code 컬럼에 저장(값이 다를 때만). 컬럼이 없으면 아무것도 하지 않음.
|
||||||
|
*
|
||||||
|
* @param list<object> $shops
|
||||||
|
* @param array<int, array{code: string, name: string}> $dongMap
|
||||||
|
*/
|
||||||
|
private function persistDesignatedShopDongCodes(array $shops, array $dongMap): void
|
||||||
|
{
|
||||||
|
if ($dongMap === [] || ! $this->designatedShopHasDongColumn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
foreach ($shops as $row) {
|
||||||
|
$idx = (int) ($row->ds_idx ?? 0);
|
||||||
|
if ($idx <= 0 || ! isset($dongMap[$idx])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$code = $dongMap[$idx]['code'];
|
||||||
|
$current = (string) ($row->ds_dong_code ?? '');
|
||||||
|
if ($code !== '' && $code !== $current) {
|
||||||
|
$db->table('designated_shop')->where('ds_idx', $idx)->update(['ds_dong_code' => $code]);
|
||||||
|
$row->ds_dong_code = $code; // 뷰 페이로드에도 반영
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 카카오맵 JavaScript SDK용 키 (.env kakao.javascriptKey) */
|
/** 카카오맵 JavaScript SDK용 키 (.env kakao.javascriptKey) */
|
||||||
private function kakaoJavascriptKey(): string
|
private function kakaoJavascriptKey(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class DesignatedShopModel extends Model
|
|||||||
'ds_rep_phone',
|
'ds_rep_phone',
|
||||||
'ds_email',
|
'ds_email',
|
||||||
'ds_gugun_code',
|
'ds_gugun_code',
|
||||||
|
'ds_dong_code',
|
||||||
'ds_zone_code',
|
'ds_zone_code',
|
||||||
'ds_branch_no',
|
'ds_branch_no',
|
||||||
'ds_designated_at',
|
'ds_designated_at',
|
||||||
|
|||||||
@@ -115,8 +115,9 @@
|
|||||||
</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>
|
||||||
<div class="text-sm text-gray-600">해당 지자체(구·군) 코드로 등록 시 자동 설정</div>
|
<input type="text" id="ds_dong_display" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60 bg-gray-100 text-gray-800 cursor-not-allowed" value="" readonly tabindex="-1" placeholder="주소 검색 시 자동 표시"/>
|
||||||
|
<span id="ds_dong_hint" class="text-xs text-gray-500">주소를 검색하면 해당 동코드가 표시됩니다.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -165,3 +166,53 @@
|
|||||||
'detailFieldName' => 'ds_addr_detail',
|
'detailFieldName' => 'ds_addr_detail',
|
||||||
]) ?>
|
]) ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var form = document.getElementById('designated-shop-create-form');
|
||||||
|
if (!form) return;
|
||||||
|
var display = document.getElementById('ds_dong_display');
|
||||||
|
var hint = document.getElementById('ds_dong_hint');
|
||||||
|
var endpoint = new URL('<?= mgmt_url('designated-shops/resolve-address-codes') ?>', window.location.href).pathname;
|
||||||
|
var csrfName = '<?= csrf_token() ?>';
|
||||||
|
|
||||||
|
function setHint(msg, isError) {
|
||||||
|
if (!hint) return;
|
||||||
|
hint.textContent = msg;
|
||||||
|
hint.className = 'text-xs ' + (isError ? 'text-red-600' : 'text-gray-500');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 주소 검색 완료 시 서버에서 동코드 조회 → 표시
|
||||||
|
form.addEventListener('kakao-address-selected', function () {
|
||||||
|
if (display) display.value = '';
|
||||||
|
setHint('동코드 조회 중…', false);
|
||||||
|
|
||||||
|
var body = new URLSearchParams();
|
||||||
|
body.set('addr_search_sido', (form.querySelector('[name="addr_search_sido"]') || {}).value || '');
|
||||||
|
body.set('addr_search_sigungu', (form.querySelector('[name="addr_search_sigungu"]') || {}).value || '');
|
||||||
|
body.set('ds_addr', (form.querySelector('[name="ds_addr"]') || {}).value || '');
|
||||||
|
body.set('ds_addr_jibun', (form.querySelector('[name="ds_addr_jibun"]') || {}).value || '');
|
||||||
|
body.set('ds_zip', (form.querySelector('[name="ds_zip"]') || {}).value || '');
|
||||||
|
var csrfEl = form.querySelector('[name="' + csrfName + '"]');
|
||||||
|
if (csrfEl) body.set(csrfName, csrfEl.value);
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
body: body
|
||||||
|
})
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (d) {
|
||||||
|
if (d && d.ok) {
|
||||||
|
if (display) display.value = d.dong_code + (d.dong_name ? ' (' + d.dong_name + ')' : '');
|
||||||
|
setHint('등록 시 판매소번호에 이 동코드가 반영됩니다.', false);
|
||||||
|
} else {
|
||||||
|
setHint((d && d.error) ? d.error : '동코드를 조회하지 못했습니다.', true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
setHint('동코드 조회 중 오류가 발생했습니다.', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -191,10 +191,8 @@ $listBasePath = $readOnly ? 'designated-shops/browse' : 'designated-shops';
|
|||||||
<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"><?= $readOnly ? '지정판매소 조회' : '지정판매소 관리' ?></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 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>
|
<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): ?>
|
<?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; ?>
|
<?php endif; ?>
|
||||||
@@ -245,6 +243,9 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0];
|
|||||||
<th class="ds-sort-th py-2.5 px-2 w-14 text-left cursor-pointer select-none" data-sort-key="no" data-sort-type="num">번호<span class="ds-sort-ind"></span></th>
|
<th class="ds-sort-th py-2.5 px-2 w-14 text-left cursor-pointer select-none" data-sort-key="no" data-sort-type="num">번호<span class="ds-sort-ind"></span></th>
|
||||||
<th class="ds-sort-th py-2.5 px-2 cursor-pointer select-none" data-sort-key="rep" data-sort-type="str">대표자명<span class="ds-sort-ind"></span></th>
|
<th class="ds-sort-th py-2.5 px-2 cursor-pointer select-none" data-sort-key="rep" data-sort-type="str">대표자명<span class="ds-sort-ind"></span></th>
|
||||||
<th class="ds-sort-th py-2.5 px-2 cursor-pointer select-none" data-sort-key="name" data-sort-type="str">상호명<span class="ds-sort-ind"></span></th>
|
<th class="ds-sort-th py-2.5 px-2 cursor-pointer select-none" data-sort-key="name" data-sort-type="str">상호명<span class="ds-sort-ind"></span></th>
|
||||||
|
<th class="ds-sort-th py-2.5 px-2 cursor-pointer select-none" data-sort-key="dong" data-sort-type="str">구·군(동코드)<span class="ds-sort-ind"></span></th>
|
||||||
|
<th class="ds-sort-th py-2.5 px-2 w-24 text-left cursor-pointer select-none" data-sort-key="designated" data-sort-type="str">지정일<span class="ds-sort-ind"></span></th>
|
||||||
|
<th class="ds-sort-th py-2.5 px-2 cursor-pointer select-none" data-sort-key="zone" data-sort-type="str">구역<span class="ds-sort-ind"></span></th>
|
||||||
<th class="ds-sort-th py-2.5 px-2 w-16 text-left cursor-pointer select-none" data-sort-key="state" data-sort-type="num">상태<span class="ds-sort-ind"></span></th>
|
<th class="ds-sort-th py-2.5 px-2 w-16 text-left cursor-pointer select-none" data-sort-key="state" data-sort-type="num">상태<span class="ds-sort-ind"></span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -275,16 +276,24 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0];
|
|||||||
if ($addrCombinedList === '') {
|
if ($addrCombinedList === '') {
|
||||||
$addrCombinedList = $addrMainList;
|
$addrCombinedList = $addrMainList;
|
||||||
}
|
}
|
||||||
|
// 구·군(동코드) 표시: 동코드가 있으면 "동코드 (동명)", 없으면 구·군명
|
||||||
|
$dongDisp = (string) (($dongDisplayMap[(int) ($row->ds_idx ?? 0)] ?? '') !== '' ? $dongDisplayMap[(int) ($row->ds_idx ?? 0)] : $ggLabel);
|
||||||
?>
|
?>
|
||||||
<tr class="ds-list-row cursor-pointer border-b border-gray-200 last:border-0 hover:bg-blue-50/60"
|
<tr class="ds-list-row cursor-pointer border-b border-gray-200 last:border-0 hover:bg-blue-50/60"
|
||||||
data-row-index="<?= (int) $i ?>" role="button" tabindex="0"
|
data-row-index="<?= (int) $i ?>" role="button" tabindex="0"
|
||||||
data-sort-no="<?= esc((string) (preg_match('/\d+/', $shortNo, $mn) ? (int) $mn[0] : 0), 'attr') ?>"
|
data-sort-no="<?= esc((string) (preg_match('/\d+/', $shortNo, $mn) ? (int) $mn[0] : 0), 'attr') ?>"
|
||||||
data-sort-rep="<?= esc($row->ds_rep_name ?? '', 'attr') ?>"
|
data-sort-rep="<?= esc($row->ds_rep_name ?? '', 'attr') ?>"
|
||||||
data-sort-name="<?= esc($row->ds_name ?? '', 'attr') ?>"
|
data-sort-name="<?= esc($row->ds_name ?? '', 'attr') ?>"
|
||||||
|
data-sort-dong="<?= esc($dongDisp, 'attr') ?>"
|
||||||
|
data-sort-designated="<?= esc($daDisp, 'attr') ?>"
|
||||||
|
data-sort-zone="<?= esc($zone, 'attr') ?>"
|
||||||
data-sort-state="<?= (int) $st ?>">
|
data-sort-state="<?= (int) $st ?>">
|
||||||
<td class="py-2.5 px-2 text-left font-mono text-gray-700"><?= esc($shortNo) ?></td>
|
<td class="py-2.5 px-2 text-left font-mono text-gray-700"><?= esc($shortNo) ?></td>
|
||||||
<td class="py-2.5 px-2 text-gray-600 ds-col-tight" title="<?= esc($row->ds_rep_name ?? '') ?>"><?= esc($row->ds_rep_name ?? '') ?></td>
|
<td class="py-2.5 px-2 text-gray-600 ds-col-tight" title="<?= esc($row->ds_rep_name ?? '') ?>"><?= esc($row->ds_rep_name ?? '') ?></td>
|
||||||
<td class="py-2.5 px-2 font-medium text-gray-900 ds-col-tight" title="<?= esc($row->ds_name ?? '') ?>"><?= esc($row->ds_name ?? '') ?></td>
|
<td class="py-2.5 px-2 font-medium text-gray-900 ds-col-tight" title="<?= esc($row->ds_name ?? '') ?>"><?= esc($row->ds_name ?? '') ?></td>
|
||||||
|
<td class="py-2.5 px-2 text-gray-600 font-mono" title="<?= esc($dongDisp) ?>"><?= esc($dongDisp) ?></td>
|
||||||
|
<td class="py-2.5 px-2 text-left text-gray-500 text-[12px]"><?= esc($daDisp) ?></td>
|
||||||
|
<td class="py-2.5 px-2 text-gray-600" title="<?= esc($zone) ?>"><?= esc($zone) ?></td>
|
||||||
<td class="py-2.5 px-2 text-left">
|
<td class="py-2.5 px-2 text-left">
|
||||||
<?php if ($st === 1): ?>
|
<?php if ($st === 1): ?>
|
||||||
<span class="inline-block px-2 py-0.5 rounded-full text-[11px] font-medium bg-emerald-50 text-emerald-700">정상</span>
|
<span class="inline-block px-2 py-0.5 rounded-full text-[11px] font-medium bg-emerald-50 text-emerald-700">정상</span>
|
||||||
@@ -570,7 +579,7 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0];
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="text-center">번호</th>
|
<th class="text-center">번호</th>
|
||||||
<th>지자체</th>
|
<th>지자체</th>
|
||||||
<th>구·군</th>
|
<th>구·군(동코드)</th>
|
||||||
<th class="text-center">지정일</th>
|
<th class="text-center">지정일</th>
|
||||||
<th>구역</th>
|
<th>구역</th>
|
||||||
<th>대표자명</th>
|
<th>대표자명</th>
|
||||||
@@ -613,8 +622,12 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0];
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($shortNoP) ?></td>
|
<td class="text-center"><?= esc($shortNoP) ?></td>
|
||||||
<td class="text-left"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
|
<td class="text-left"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
|
||||||
<?php $gCodeP = (string) ($row->ds_gugun_code ?? ''); ?>
|
<?php
|
||||||
<td class="text-left"><?= esc((string) (($gugunNameMap[$gCodeP] ?? '') !== '' ? $gugunNameMap[$gCodeP] : $gCodeP)) ?></td>
|
$gCodeP = (string) ($row->ds_gugun_code ?? '');
|
||||||
|
$ggLabelP = (string) (($gugunNameMap[$gCodeP] ?? '') !== '' ? $gugunNameMap[$gCodeP] : $gCodeP);
|
||||||
|
$dongDispP = (string) (($dongDisplayMap[(int) ($row->ds_idx ?? 0)] ?? '') !== '' ? $dongDisplayMap[(int) ($row->ds_idx ?? 0)] : $ggLabelP);
|
||||||
|
?>
|
||||||
|
<td class="text-left"><?= esc($dongDispP) ?></td>
|
||||||
<td class="text-center"><?= esc($daDispP) ?></td>
|
<td class="text-center"><?= esc($daDispP) ?></td>
|
||||||
<td class="text-left"><?= esc($row->ds_zone_code ?? '') ?></td>
|
<td class="text-left"><?= esc($row->ds_zone_code ?? '') ?></td>
|
||||||
<td class="text-left"><?= esc($row->ds_rep_name ?? '') ?></td>
|
<td class="text-left"><?= esc($row->ds_rep_name ?? '') ?></td>
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ $roadBaseOnly = ! empty($roadBaseOnly);
|
|||||||
var detEl = field(detailFieldName);
|
var detEl = field(detailFieldName);
|
||||||
if (detEl) detEl.value = data.buildingName || '';
|
if (detEl) detEl.value = data.buildingName || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 주소가 채워진 뒤, 폼에서 후속 처리(예: 동코드 조회)를 할 수 있도록 이벤트 발행
|
||||||
|
try {
|
||||||
|
form.dispatchEvent(new CustomEvent('kakao-address-selected', { detail: data }));
|
||||||
|
} catch (e) { /* 구형 브라우저 무시 */ }
|
||||||
}
|
}
|
||||||
}).open();
|
}).open();
|
||||||
});
|
});
|
||||||
|
|||||||
16
writable/database/designated_shop_add_dong_code.sql
Normal file
16
writable/database/designated_shop_add_dong_code.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- 지정판매소에 동코드(code_detail 종류 D) 저장 컬럼 추가
|
||||||
|
-- ds_gugun_code(구코드) 다음에 ds_dong_code(동코드)를 둔다.
|
||||||
|
-- 실행: mysql -u jongryangje -p jongryangje_dev < writable/database/designated_shop_add_dong_code.sql
|
||||||
|
-- 멱등 실행(이미 있으면 건너뜀).
|
||||||
|
|
||||||
|
SET @db = DATABASE();
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'designated_shop' AND COLUMN_NAME = 'ds_dong_code') > 0,
|
||||||
|
'SELECT ''ds_dong_code already exists'' AS msg',
|
||||||
|
'ALTER TABLE `designated_shop` ADD COLUMN `ds_dong_code` VARCHAR(20) NOT NULL DEFAULT '''' COMMENT ''동코드 code_detail(D)'' AFTER `ds_gugun_code`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
Reference in New Issue
Block a user