diff --git a/app/Commands/NaverWorker.php b/app/Commands/NaverWorker.php index 3ec7703..bcb265e 100644 --- a/app/Commands/NaverWorker.php +++ b/app/Commands/NaverWorker.php @@ -15,10 +15,14 @@ class NaverWorker extends BaseCommand protected $name = 'naver:worker'; protected $description = 'Redis에서 데이터를 꺼내 DB에 저장하고 네이버 API를 호출합니다.'; + // DB 객체를 담을 변수 선언 + protected $db; + public function run(array $params) { helper('log'); // 여기서 로드 완료! + $this->db = \Config\Database::connect(); $logModel = model(NaverWorkerLogModel::class); $naverService = new \App\Services\NaverService(); // 서비스 생성 @@ -35,6 +39,17 @@ class NaverWorker extends BaseCommand while (true) { + + // 1. DB 연결 상태 체크 (더 견고하게) + try { + if ($this->db->connID === false || !@$this->db->connID->ping()) { + $this->db->reconnect(); + CLI::write(CLI::color('🔄 Database reconnected.', 'yellow')); + } + } catch (\Throwable $e) { + $this->db->reconnect(); + } + $result = $redis->brPop(['naver:raw_queue'], 30); if (!$result) { @@ -81,6 +96,9 @@ class NaverWorker extends BaseCommand $redis->lPush('naver:failed_queue', $rawData); helper('log'); write_custom_log("FAILED_DATA | Error: " . $e->getMessage(), 'ERROR', 'failed'); + + // 루프 과부하 방지 (연속 에러 시) + sleep(1); } } } diff --git a/app/Services/NaverService.php b/app/Services/NaverService.php index a4819d5..da7d004 100644 --- a/app/Services/NaverService.php +++ b/app/Services/NaverService.php @@ -5,6 +5,7 @@ namespace App\Services; use CodeIgniter\CLI\CLI; use App\Libraries\NaverApiClient; use App\Models\Entities\VrfcReqModel; +use App\Models\Entities\V2ArticleInfoModel; use App\Models\Entities\V2stdailyModel; use App\Models\Entities\NaverRawStagingModel; use App\Models\Entities\ReceiptModel; @@ -15,7 +16,7 @@ use Exception; class NaverService { protected $db; - protected $naverClient, $VrfcReqModel, $V2stdailyModel, $statusService, $rawStagingModel, $receiptModel, $resultModel; + protected $naverClient, $VrfcReqModel, $V2stdailyModel, $statusService, $rawStagingModel, $receiptModel, $resultModel , $articleModel; public function __construct() { @@ -23,8 +24,10 @@ class NaverService $this->naverClient = new NaverApiClient(); $this->VrfcReqModel = model(VrfcReqModel::class); + $this->articleModel = model(V2ArticleInfoModel::class); $this->V2stdailyModel = model(V2stdailyModel::class); $this->rawStagingModel = model(NaverRawStagingModel::class); + $this->receiptModel = model(ReceiptModel::class); $this->resultModel = model(ResultModel::class); @@ -248,29 +251,276 @@ class NaverService throw $e; } } - public function processTypeV2($articleNumber, $rawData, $payload){ - + + /** + * [Type V2] 일반/서류/비공동주택 처리 로직 + */ + private function processTypeV2($articleNumber, $rawData, $payload) + { + + $vrfcParam = $this->v2Parameter($articleNumber, $rawData, $payload); + $articleInfoParam = $this->articleInfoParameter($articleNumber, $rawData, $payload); + try { + + switch ($payload['requestType']){ + case "REG": + + $vr_sq = $this->insertVrfcReq($vrfcParam); + $articleInfoParam['vr_sq'] = $vr_sq; + write_custom_log("articleInfoParam :: " . json_encode($articleInfoParam, JSON_UNESCAPED_UNICODE) , "INFO", "SERVICE"); + + // 인서트 실행 + if ($this->articleModel->insert($articleInfoParam)) { + // 성공 로그 + write_custom_log("articleInfo Insert Success :: vr_sq: $vr_sq", "INFO", "SERVICE"); + } else { + // 1. 모델에서 발생한 에러 정보 가져오기 + $dbError = $this->db->error(); + $lastQuery = (string)$this->articleModel->getLastQuery(); + + // 2. 로그 기록 (에러 메시지 + 실패한 쿼리) + write_custom_log( + "DB_ERROR | Code: {$dbError['code']} | Message: {$dbError['message']} | Query: {$lastQuery}", + "ERROR", + "SERVICE" + ); + + // 필요하다면 예외를 던져서 상위(Worker)의 try-catch에서 처리하게 함 + throw new \Exception("ArticleInfo Insert Failed: " . $dbError['message']); + } + + break; + case "MOD": + break; + case "CNC": + break; + } + + } catch (\Exception $e) { + write_custom_log("CRITICAL_ERROR :: " . $e->getMessage(), "ERROR", "SERVICE"); + + throw $e; + } } + + private function v2Parameter($articleNumber, $rawData , $payload){ + $now = db_now(); + $step = isset($rawData['step']) ? $rawData['step'] : '00'; + $rdate = $payload['requestDate'] ?? db_now('YmdHis'); + $insert_user = 0; + $stat_cd = '10'; + $sync_yn = 'N'; + $insert_tm = $now; + $reg_type = function($payload) { + switch ($payload['requestType'] ?? '') { + case 'REG': return 'C'; + case 'MOD': return 'U'; + case 'CNC': return 'D'; + default: return '0'; + } + };($payload); + $files = $rawData['files'] ?? []; + $certRegister = []; + $confirm_doc_img_url = []; + $referenceFileUrl = []; + + foreach ($files as $file) { + $fileTypeCode = $file['fileTypeCode']; + if ($fileTypeCode == 'RCDOC') { + $certRegister[] = $file['fileUrl']; + } elseif ($fileTypeCode == 'ADDOC') { + $confirm_doc_img_url[] = $file['fileUrl']; + } elseif ($fileTypeCode == 'REFER') { + $referenceFileUrl[] = $file['fileUrl']; + } + } + + // 1. v2_vrfc_req (검증요청) 데이터 준비 + $vrfcReqData = [ + 'reqSeq' => '', // 네이버 요청 고유 ID + 'atcl_no' => $articleNumber, + 'step' => $step, // 기본 단계 설정 + 'cpid' => $rawData['cpId'] ?? 'naver', + 'cp_atcl_id' => $rawData['cpArticleNumber'] ?? '', + 'trade_type' => $rawData['tradeTypeCode'] ?? '', + 'realtor_nm' => $rawData['realtor']['realtorName'] ?? null, + 'realtor_tel_no' => $rawData['realtor']['representativeCellphoneNumber'] ?? null, + 'seller_tel_no' => $rawData['seller']['cellphoneNumber'] ?? null, + 'vrfc_type' => $rawData['verificationTypeCode'] ?? 'D', // D, T, P 등 + 'rgbk_confirm' => null, + 'req_type' => $reg_type($payload), // 등록:C, 수정:U, 취소:D + 'rdate' => $rdate, + 'cpTelNo' => null, + 'stat_cd' => $stat_cd, // 초기 대기 상태 + 'insert_user' => $insert_user, + 'insert_tm' => $insert_tm, + 'sync_yn' => $sync_yn, + 'rgbk_confirm_owner_nm' => null, + 'direct_trad_yn' => ($rawData['seller']['isDirectTrade'] ?? false) ? 'Y' : 'N', + 'confirm_doc_img_url' => $confirm_doc_img_url, + 'confirm_doc_owner_check_yn' => null, + 'certRegister' => json_encode($certRegister), + 'referenceFileUrl' => json_encode($referenceFileUrl), + ]; + + return $vrfcReqData; + } + + private function articleInfoParameter($articleNumber, $rawData , $payload){ + + // JSON 객체 안전하게 로드 + $address = $rawData['address'] ?? []; + $space = $rawData['space'] ?? []; + $price = $rawData['price'] ?? []; + $floor = $rawData['floor'] ?? []; + $seller = $rawData['seller'] ?? []; + $realtor = $rawData['realtor'] ?? []; + $files = $rawData['realtor']['file'] ?? []; + $certRegister = []; + $confirm_doc_img_url = []; + $referenceFileUrl = []; + + foreach ($files as $file) { + $fileTypeCode = $file['fileTypeCode']; + if ($fileTypeCode == 'RCDOC') { + $certRegister[] = $file['fileUrl']; + } elseif ($fileTypeCode == 'ADDOC') { + $confirm_doc_img_url[] = $file['fileUrl']; + } elseif ($fileTypeCode == 'REFER') { + $referenceFileUrl[] = $file['fileUrl']; + } + } + + $ownerTypeCode = null; + switch ($rawData['ownerTypeCode']) { + case "INDIV": + $ownerTypeCode = 0; + break; + case "CORP": + $ownerTypeCode = 1; + break; + case "FRGNR": + $ownerTypeCode = 2; + break; + case "DELEG": + $ownerTypeCode = 3; + break; + } + + + + $ownerCheckYn = ($seller['isOwnerCertificationAgree'] ?? false) ? 'Y' : 'N'; + + return [ + // 'vr_sq' => $vr_sq, + 'atcl_no' => $articleNumber, + 'cpid' => $rawData['cpId'] ?? null, + 'cp_atcl_id' => $rawData['cpArticleNumber'] ?? null, + 'rlet_type_cd' => $rawData['realEstateTypeCode'] ?? null, + 'trade_type' => $rawData['tradeTypeCode'] ?? null, + 'address_code' => $address['legalDivision']['sectorNumber'] ?? null, + 'address1' => $address['complexName'] ?? null, + 'address2' => trim(($address['buildingName'] ?? '') . ' ' . ($address['hoName'] ?? '')), + 'address3' => $address['legalDivision']['legalDivisionAddress'] ?? null, + + // 면적 및 가격 (데이터 없으면 null) + 'sply_spc' => $space['supplySpace'] ?? null, + 'excls_spc' => $space['exclusiveSpace'] ?? null, + 'tot_spc' => $space['totalSpace'] ?? null, + 'grnd_spc' => $space['groundSpace'] ?? null, + 'bldg_spc' => $space['buildingSpace'] ?? null, + 'deal_amt' => $price['dealAmount'] ?? 0, + 'wrrnt_amt' => $price['warrantyAmount'] ?? 0, + 'lease_amt' => $price['leaseAmount'] ?? 0, + 'isale_amt' => $price['preSaleAmount'] ?? 0, + 'prem_amt' => $price['premiumAmount'] ?? 0, + 'sise' => null, + + // 층 및 일정 + 'floor' => $floor['correspondenceFloorCount'] ?? null, + 'floor2' => $floor['totalFloorCount'] ?? null, + 'rdate' => date("Y-m-d H:i:s" , strtotime( $payload['requestDatetime'] )), + + // 셀러 + 'seller_tel_no' => $seller['sellerTelephoneNumber'] ?? null, // JSON seller 객체에 연락처 필드 부재 + 'seller_nm' => $seller['sellerName'] ?? null, + + // 중개업소 + 'realtor_nm' => $realtor['realtorName'] ?? null, + 'realtor_tel_no' => $realtor['representativeTelephoneNumber'] ?? null, + + // 단지 정보 + 'hscp_no' => $address['complexNumber'] ?? null, + 'hscp_nm' => $address['complexName'] ?? null, + 'ptp_no' => $address['pyeongTypeNumber'] ?? null, + 'ptp_nm' => $address['pyeongTypeNumber'] ?? null, + + // 담당자 + 'charger' => null, // 담당자 + 'reg_price_yn' => 'N', // 가격수정요청여부 + 'reg_charger' => null, // 등기부등본 담당자 + 'dept1_sq' => null, // 부서(본부) + 'dept2_sq' => null, // 부서(팀) + 'reg_dept2_sq' => null, // 부서(팀) + 'reg_dept1_sq' => null, // 부서(본부) + + // 상태 및 기타 + 'dong_ho_chk' => ($address['isDongHoCheck'] ?? false) ? 'Y' : 'N', + 'hscplqry_lv' => $address['inquiryLevel'] ?? null, + + // 소유자 + 'ownerNm' => $seller['ownerName'] ?? null, + 'ownerTelNo' => $seller['ownerName'] ?? null, + + // + 'chg_trade_type' => null, + 'chg_address2' => null, + 'chg_address3' => null, + 'chg_seller_tel' => null, + 'chg_amt' => null, + + 'reg_status' => null, + 'cupnNo' => null, + 'roomSiteAtclRgstCnt' => null, + 'rootSiteAtclExpsCnt' => null, + 'redvlp_area_nm' => $adress['redevelopAreaName'] ?? null, + 'biz_stp_desc' => $address['bizStepDescription'] ?? null, + + // file + 'cert_register' => json_encode($certRegister, JSON_UNESCAPED_UNICODE), + 'direct_trad_yn' => ($seller['isDirectTrade'] ?? false) ? 'Y' : 'N', + 'confirm_doc_img_url' => json_encode( $confirm_doc_img_url , JSON_UNESCAPED_UNICODE), + 'confirm_doc_owner_check_yn' => $ownerCheckYn, + 'owner_birth' => null, + 'vrfc_type_sub' => ($rawData['verificationTypeCode'] === 'D') ? ($ownerCheckYn === 'Y' ? 'D2' : 'D1') : ($rawData['verificationTypeCode'] === 'N' ? 'N2' : $rawData['verificationTypeCode'] . '1'), + 'cert_register_save_yn' => 'N', + 'confirm_doc_img_url_save_yn' => 'N', + 'address4' => $address['etcAddress'], + 'reference_file_url' => !empty($referenceFileUrl) ? implode('|', $referenceFileUrl) : '', + 'reference_file_url_save_yn' => !empty($referenceFileUrl) ? 'Y' : 'N', + 'registerBookUniqueNo' => $rawData['verificationReference'] ?? null, + 'relationSellerAndOwner' => null, + 'ownerTypeCode' => $ownerTypeCode, + 'registerBookUniqueNumber' => null + ]; + } /** * [REG] 신규 등록 */ - private function insertVrfcReq($articleNumber, $params) + private function insertVrfcReq($vrfcParam) { CLI::write(CLI::color('🟢 매물 정보 시작', 'green')); - $existing = $this->VrfcReqModel->where('atcl_no', $articleNumber)->first(); - if ($existing) throw new \Exception("중복 등록 시도: $articleNumber"); + $existing = $this->VrfcReqModel->where('atcl_no', $vrfcParam['atcl_no'])->first(); + if ($existing) throw new \Exception("중복 등록 시도: " . $vrfcParam['atcl_no']); - $params['stat_cd'] = '10'; - $params['insert_user'] = '0'; - $params['req_type'] = 'C'; - - if (!$this->VrfcReqModel->insert($params)) { + if (!$this->VrfcReqModel->insert($vrfcParam)) { + $dbError = $this->VrfcReqModel->db()->error(); // DB 에러 코드와 메시지 가져오기 $sql = (string)$this->VrfcReqModel->getLastQuery(); - write_custom_log("INSERT_FAILED | Atcl: $articleNumber | SQL: $sql", 'ERROR', 'failed'); - throw new \Exception("신규 등록 실패"); + write_custom_log("INSERT_FAILED | Atcl: " . $vrfcParam['atcl_no'] . " | Error: {$dbError['message']} | SQL: ".$sql, 'ERROR', 'failed'); + throw new \Exception("신규 등록 실패: " . $dbError['message']); } $vr_sq = $this->VrfcReqModel->getInsertID(); @@ -279,6 +529,10 @@ class NaverService return $vr_sq; } + private function insertArticleInfo($article){ + + } + /** * [MOD] 수정 처리 */