diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 91d03b2..a35fc34 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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', 'Admin\DesignatedShop::index'); $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->get('designated-shops/edit/(:num)', 'Admin\DesignatedShop::edit/$1'); $routes->post('designated-shops/update/(:num)', 'Admin\DesignatedShop::update/$1'); diff --git a/app/Controllers/Admin/DesignatedShop.php b/app/Controllers/Admin/DesignatedShop.php index a635a44..51af1c2 100644 --- a/app/Controllers/Admin/DesignatedShop.php +++ b/app/Controllers/Admin/DesignatedShop.php @@ -14,6 +14,7 @@ class DesignatedShop extends BaseController private DesignatedShopModel $shopModel; private LocalGovernmentModel $lgModel; private Roles $roles; + private ?bool $dongColumnExists = null; public function __construct() { @@ -223,9 +224,10 @@ class DesignatedShop extends BaseController /** * @param list $list * @param array $lgMap + * @param array $dongMap ds_idx => [동코드, 동명] * @return list> */ - private function buildDesignatedShopDetailPayload(array $list, array $lgMap): array + private function buildDesignatedShopDetailPayload(array $list, array $lgMap, array $dongMap = []): array { helper('admin'); $lgIdx = admin_effective_lg_idx() ?? 0; @@ -244,6 +246,18 @@ class DesignatedShop extends BaseController $stateMap = [1 => '정상', 2 => '폐업', 3 => '직권해지']; $da = $row->ds_designated_at ?? null; $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[] = [ 'ds_idx' => (int) $row->ds_idx, 'ds_shop_no' => $sn, @@ -266,7 +280,10 @@ class DesignatedShop extends BaseController 'ds_rep_phone' => (string) ($row->ds_rep_phone ?? ''), 'ds_email' => (string) ($row->ds_email ?? ''), '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_branch_no' => $this->designatedShopScalar($row, 'ds_branch_no'), 'ds_designated_at' => $daOut, @@ -300,8 +317,9 @@ class DesignatedShop extends BaseController $dsState = $this->request->getGet('ds_state'); $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 = []; @@ -311,7 +329,21 @@ class DesignatedShop extends BaseController $stateCounts = $this->countDesignatedShopsByState($lgIdx, $dsName, $dsGugunCode, $dsState); $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(); @@ -327,6 +359,7 @@ class DesignatedShop extends BaseController 'gugunCodes' => $gugunCodes, 'stateCounts' => $stateCounts, 'gugunNameMap' => $gugunNameMap, + 'dongDisplayMap' => $dongDisplayMap, 'detailRowsJson' => json_encode($detailRows, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE), 'kakaoJavascriptKey' => $this->kakaoJavascriptKey(), ]; @@ -477,14 +510,25 @@ class DesignatedShop extends BaseController $list = $this->shopModel->where('ds_lg_idx', $lgIdx)->orderBy('ds_idx', 'DESC')->findAll(); + // 동코드(구·군 동코드) 산출 + $dongMap = $this->resolveDongCodesForShops($lgIdx, $list); + $rows = []; foreach ($list as $row) { $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[] = [ $row->ds_idx, $row->ds_shop_no, $row->ds_name, $row->ds_rep_name, + $dongDisp, + $daDisp, $row->ds_biz_no, $this->designatedShopScalar($row, 'ds_biz_type'), $this->designatedShopScalar($row, 'ds_biz_kind'), @@ -504,7 +548,7 @@ class DesignatedShop extends BaseController export_csv( '지정판매소_' . date('Ymd') . '.csv', [ - '번호', '판매소번호', '상호명', '대표자', '사업자번호', '업태', '업종', + '번호', '판매소번호', '상호명', '대표자', '구·군(동코드)', '지정일', '사업자번호', '업태', '업종', '가상계좌은행', '계좌번호', '전화번호', '주소', '구역', '종사업장번호', '변경일자', '변경사유', '상태', '등록일', ], @@ -648,6 +692,8 @@ class DesignatedShop extends BaseController 'ds_branch_no' => (string) $this->request->getPost('ds_branch_no'), 'ds_designated_at' => $this->request->getPost('ds_designated_at') ?: null, '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_change_reason' => (string) $this->request->getPost('ds_change_reason'), 'ds_regdate' => date('Y-m-d H:i:s'), @@ -770,6 +816,23 @@ class DesignatedShop extends BaseController $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 = [ 'ds_name' => (string) $this->request->getPost('ds_name'), '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_rep_phone' => (string) $this->request->getPost('ds_rep_phone'), 'ds_email' => (string) $this->request->getPost('ds_email'), + ...$dongCodeData, 'ds_zone_code' => (string) $this->request->getPost('ds_zone_code'), 'ds_branch_no' => (string) $this->request->getPost('ds_branch_no'), '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']); $dCode = null; + $dName = ''; foreach ($dCandidates as $cand) { if ($this->addressHaystackContainsRegionName($blob, $cand['nm'])) { $dCode = $cand['cd']; + $dName = $cand['nm']; break; } } @@ -1557,9 +1623,203 @@ class DesignatedShop extends BaseController 'ok' => true, 'shop_no' => $prefix . sprintf('%03d', $maxSerial + 1), '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 $shops + * @return array 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 $shops + * @param array $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) */ private function kakaoJavascriptKey(): string { diff --git a/app/Models/DesignatedShopModel.php b/app/Models/DesignatedShopModel.php index f7bcd72..5890dbd 100644 --- a/app/Models/DesignatedShopModel.php +++ b/app/Models/DesignatedShopModel.php @@ -33,6 +33,7 @@ class DesignatedShopModel extends Model 'ds_rep_phone', 'ds_email', 'ds_gugun_code', + 'ds_dong_code', 'ds_zone_code', 'ds_branch_no', 'ds_designated_at', diff --git a/app/Views/admin/designated_shop/create.php b/app/Views/admin/designated_shop/create.php index ebec451..976db08 100644 --- a/app/Views/admin/designated_shop/create.php +++ b/app/Views/admin/designated_shop/create.php @@ -115,8 +115,9 @@
- -
해당 지자체(구·군) 코드로 등록 시 자동 설정
+ + + 주소를 검색하면 해당 동코드가 표시됩니다.
@@ -165,3 +166,53 @@ 'detailFieldName' => 'ds_addr_detail', ]) ?> + + diff --git a/app/Views/admin/designated_shop/index.php b/app/Views/admin/designated_shop/index.php index b2e7411..96cc615 100644 --- a/app/Views/admin/designated_shop/index.php +++ b/app/Views/admin/designated_shop/index.php @@ -191,10 +191,8 @@ $listBasePath = $readOnly ? 'designated-shops/browse' : 'designated-shops';
- 엑셀저장 - 지정판매소 등록 @@ -245,6 +243,9 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0]; 번호 대표자명 상호명 + 구·군(동코드) + 지정일 + 구역 상태 @@ -275,16 +276,24 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0]; if ($addrCombinedList === '') { $addrCombinedList = $addrMainList; } + // 구·군(동코드) 표시: 동코드가 있으면 "동코드 (동명)", 없으면 구·군명 + $dongDisp = (string) (($dongDisplayMap[(int) ($row->ds_idx ?? 0)] ?? '') !== '' ? $dongDisplayMap[(int) ($row->ds_idx ?? 0)] : $ggLabel); ?> ds_rep_name ?? '') ?> ds_name ?? '') ?> + + + 정상 @@ -570,7 +579,7 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0]; 번호 지자체 - 구·군 + 구·군(동코드) 지정일 구역 대표자명 @@ -613,8 +622,12 @@ $sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0]; ds_lg_idx] ?? '') ?> - ds_gugun_code ?? ''); ?> - + 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); + ?> + ds_zone_code ?? '') ?> ds_rep_name ?? '') ?> diff --git a/app/Views/components/kakao_address_search.php b/app/Views/components/kakao_address_search.php index e93b12d..591ec5c 100644 --- a/app/Views/components/kakao_address_search.php +++ b/app/Views/components/kakao_address_search.php @@ -112,6 +112,11 @@ $roadBaseOnly = ! empty($roadBaseOnly); var detEl = field(detailFieldName); if (detEl) detEl.value = data.buildingName || ''; } + + // 주소가 채워진 뒤, 폼에서 후속 처리(예: 동코드 조회)를 할 수 있도록 이벤트 발행 + try { + form.dispatchEvent(new CustomEvent('kakao-address-selected', { detail: data })); + } catch (e) { /* 구형 브라우저 무시 */ } } }).open(); }); diff --git a/writable/database/designated_shop_add_dong_code.sql b/writable/database/designated_shop_add_dong_code.sql new file mode 100644 index 0000000..d37ff6e --- /dev/null +++ b/writable/database/designated_shop_add_dong_code.sql @@ -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;