diff --git a/app/Commands/NaverWorker.php b/app/Commands/NaverWorker.php index 18e73a1..3ec7703 100644 --- a/app/Commands/NaverWorker.php +++ b/app/Commands/NaverWorker.php @@ -74,6 +74,11 @@ class NaverWorker extends BaseCommand } catch (\Exception $e) { CLI::error("❌ Task Failed: " . $e->getMessage()); // 실패 로그는 여기서 남김 + // 1. DB 상태를 FAIL로 업데이트 (필수) + $logModel->update($logId, ['status' => 'FAIL', 'error_msg' => $e->getMessage()]); + + // 2. Redis 실패 큐에 백업 (선택 - 나중에 모아서 다시 던질 때 편함) + $redis->lPush('naver:failed_queue', $rawData); helper('log'); write_custom_log("FAILED_DATA | Error: " . $e->getMessage(), 'ERROR', 'failed'); } diff --git a/app/Helpers/function_helper.php b/app/Helpers/function_helper.php index 1ac4ad9..1829f40 100644 --- a/app/Helpers/function_helper.php +++ b/app/Helpers/function_helper.php @@ -283,3 +283,27 @@ function checkPasswordTypes(string $password, int $minTypes = 2): bool return $types >= $minTypes; } + +/** + * 소유자 구분코드 변환 + */ +function getOwnerTypeCodeNo($code) +{ + switch ($code) { + case "INDIV": + return "0"; + break; + case "CORP": + return "1"; + break; + case "FRGNR": + return "2"; + break; + case "DELEG": + return "3"; + break; + default: + return null; + break; + } +} diff --git a/app/Libraries/NaverApiClient.php b/app/Libraries/NaverApiClient.php index 0fca11a..e6530ad 100644 --- a/app/Libraries/NaverApiClient.php +++ b/app/Libraries/NaverApiClient.php @@ -79,7 +79,7 @@ class NaverApiClient curl_close($ch); // 결과 로그 기록 (성공/실패 모두 기록하여 추적 가능하게 함) - if ($httpCode === 200 && $httpdCode === 202) { + if ($httpCode === 200 || $httpCode === 202) { log_message('info', "[Naver API $method SUCCESS] URL: $url | Response: $response"); } else { log_message('error', "[Naver API $method FAIL] URL: $url | Code: $httpCode | Response: $response"); diff --git a/app/Services/NaverService.php b/app/Services/NaverService.php index 150d37b..d5162ce 100644 --- a/app/Services/NaverService.php +++ b/app/Services/NaverService.php @@ -5,20 +5,19 @@ namespace App\Services; use App\Libraries\NaverApiClient; use App\Models\Entities\VrfcReqModel; use App\Models\Entities\V2stdailyModel; -use App\Models\Entities\V2chgstatModel; -use App\Models\Entities\V2chghistoryModel; +use App\Services\StatusService; // 추가 +use Exception; class NaverService { - protected $naverClient, $VrfcReqModel, $V2stdailyModel, $V2chgstatModel, $V2chghistoryModel; + protected $naverClient, $VrfcReqModel, $V2stdailyModel, $statusService; public function __construct() { $this->naverClient = new NaverApiClient(); $this->VrfcReqModel = model(VrfcReqModel::class); $this->V2stdailyModel = model(V2stdailyModel::class); - $this->V2chgstatModel = model(V2chgstatModel::class); - $this->V2chghistoryModel = model(V2chghistoryModel::class); + $this->statusService = new StatusService(); // 인스턴스 생성 helper('log'); } @@ -55,9 +54,10 @@ class NaverService if ($vr_sq) $this->V2stdailyModel->set_v2_st_daily(null, $vrfcParams['cpid'], 'A0101', '1', 'add'); break; - case 'FIN': // 완료 - $vr_sq = $this->finVrfcReq($articleNumber, $vrfcParams); - break; + // case 'FIN': // 완료 + // $vr_sq = $this->finVrfcReq($articleNumber, $vrfcParams); + // if ($vr_sq) $this->V2stdailyModel->set_v2_st_daily(null, $vrfcParams['cpid'], 'A0101', '1', 'add'); + // break; default: throw new \Exception("알 수 없는 requestType: $requestType"); @@ -85,7 +85,7 @@ class NaverService } $vr_sq = $this->VrfcReqModel->getInsertID(); - $this->recordStatusAndHistory($vr_sq, '10', 'C9', "신규접수 : 10"); + $this->statusService->recordStatusAndHistory($vr_sq, '10', 'C9', "신규접수 : 10"); return $vr_sq; } @@ -95,8 +95,22 @@ class NaverService */ private function updateVrfcReq($articleNumber, $params) { - $existing = $this->findExisting($articleNumber); - if (!$existing) return $this->insertVrfcReq($articleNumber, $params); + + // 1. 데이터 존재 여부 확인 + $existing = $this->VrfcReqModel->where('atcl_no', $articleNumber)->first(); + + if (!$existing) { + // [A] 데이터가 없으면 먼저 신규 등록(10) 실행 + write_custom_log("MOD_NOT_FOUND | Atcl: $articleNumber | First, Insert new data.", 'INFO', 'service'); + + $vr_sq = $this->insertVrfcReq($articleNumber, $params); + + // 새로 등록된 정보를 다시 가져옴 (updateProcess를 태우기 위해) + $existing = $this->VrfcReqModel->find($vr_sq); + + // 통계 기록 (신규 등록 건으로 카운트) + $this->V2stdailyModel->set_v2_st_daily(null, $params['cpid'], $params['vrfc_type'] . '0103', '1', 'add'); + } $params['stat_cd'] = '30'; $params['req_type'] = 'U'; @@ -110,7 +124,13 @@ class NaverService */ private function deleteVrfcReq($articleNumber, $params) { - $existing = $this->findExisting($articleNumber); + try { + $existing = $this->findExisting($articleNumber); + } catch (\Exception $e) { + // 취소(CNC)하려는데 데이터가 없으면 이미 지워졌거나 오류일 수 있습니다. + // 여기서는 로그만 남기고 종료하거나, 필요 시 Exception을 던집니다. + throw new \Exception("CNC_NOT_FOUND | Atcl: $articleNumber WARNING service"); + } $params['stat_cd'] = '19'; $params['req_type'] = 'D'; @@ -122,7 +142,13 @@ class NaverService */ private function finVrfcReq($articleNumber, $params) { - $existing = $this->findExisting($articleNumber); + try { + $existing = $this->findExisting($articleNumber); + } catch (\Exception $e) { + // 완료(FIN)하려는데 데이터가 없으면 이미 지워졌거나 오류일 수 있습니다. + // 여기서는 로그만 남기고 종료하거나, 필요 시 Exception을 던집니다. + throw new \Exception("FIN_NOT_FOUND | Atcl: $articleNumber WARNING service"); + } $params['stat_cd'] = '60'; $params['req_type'] = 'F'; @@ -133,7 +159,7 @@ class NaverService private function findExisting($articleNumber) { $existing = $this->VrfcReqModel->where('atcl_no', $articleNumber)->first(); - if (!$existing) throw new \Exception("해당 매물 없음: $articleNumber"); + if (!$existing) throw new \Exception("해당 매물 없음: $articleNumber 재등록 필요"); return $existing; } @@ -150,34 +176,10 @@ class NaverService throw new \Exception("[$type] 업데이트 실패"); } - $this->recordStatusAndHistory($vr_sq, $params['stat_cd'], 'C9', $memo); + $this->statusService->recordStatusAndHistory($vr_sq, $params['stat_cd'], 'C9', $memo); return $vr_sq; } - /** - * 상태 및 이력 테이블 기록 (독립적 에러 처리) - */ - private function recordStatusAndHistory($vr_sq, $stat_cd, $chg_type, $memo) - { - // 1. 상태(stat) 저장 - try { - $this->V2chgstatModel->saveChgstat([ - 'vr_sq' => $vr_sq, 'stat_cd' => $stat_cd, 'insert_user' => '0', 'insert_tm' => db_now() - ], 'I'); - } catch (\Exception $e) { - write_custom_log("STAT_SAVE_ERR | vr_sq: $vr_sq | Msg: " . $e->getMessage(), 'ERROR', 'failed'); - } - - // 2. 이력(history) 저장 - try { - $this->V2chghistoryModel->v2_savehistory([ - 'vr_sq' => $vr_sq, 'stat_cd' => $stat_cd, 'chg_type' => $chg_type, - 'memo' => $memo, 'insert_id' => 'SYSTEM', 'insert_tm' => db_now() - ]); - } catch (\Exception $e) { - write_custom_log("HIST_SAVE_ERR | vr_sq: $vr_sq | Msg: " . $e->getMessage(), 'ERROR', 'failed'); - } - } /** * API 데이터를 DB 컬럼에 맞게 변환 @@ -236,8 +238,114 @@ class NaverService 'referenceFileUrl' => empty($referenceFileUrl) ? null : json_encode($referenceFileUrl, JSON_UNESCAPED_UNICODE), ]; + $articl_info_param = [ + 'address_code' => $articleInfo['address']['legalDivision']['sectorNumber'] ?? '', //읍면동코드 + 'address1' => $articleInfo['address']['legalDivision']['legalDivisionAddress'] ?? '', //법정도 주소 + 'address2' => $articleInfo['address']['jibunAddress'] ?? '', + 'address3' => $articleInfo['address']['referenceAddress'] ?? '', + 'address4' => $articleInfo['address']['etcAddress'] ?? '', + 'sply_spc' => $articleInfo['space']['supplySpace'] ?? 0, + 'excls_spc' => $articleInfo['space']['exclusiveSpace'] ?? 0, + 'tot_spc' => $articleInfo['space']['totalSpace'] ?? 0, + 'grnd_spc' => $articleInfo['space']['groundSpace'] ?? 0, + 'bldg_spc' => $articleInfo['space']['buildingSpace'] ?? 0, + 'deal_amt' => $articleInfo['price']['dealAmount'] ?? 0, // 매매금액 + 'wrrnt_amt' => $articleInfo['price']['warrantyAmount'] ?? 0, // 보증금 + 'lease_amt' => $articleInfo['price']['leaseAmount'] ?? 0, // 월세가 -> 임대가 + 'isale_amt' => $articleInfo['price']['preSaleAmount'] ?? 0, // 분양가 + 'prem_amt' => $articleInfo['price']['premiumAmount'] ?? 0, // 프리미엄 + 'sise' => null, // 매매, 전세 시세 + 'floor' => $articleInfo['floor']['correspondenceFloorCount'] ?? 0, // 층 / 해당 층 + '_correspondenceFloorType' => $articleInfo['floor']['correspondenceFloorType'] ?? null, // 층 / 해당 층 타입 // 기존 없음 + 'floor2' => $articleInfo['floor']['totalFloorCount'] ?? 0, // 층 / 총 층수 // 기존 없음 + '_undergroundFloorCount' => $articleInfo['floor']['undergroundFloorCount'] ?? 0, // 층 / 지하 층수 // 기존 없음 + 'rdate' => $requestDatetime ?? db_now('Y-m-d H:i:s'), + 'seller_tel_no' => $articleInfo['seller']['sellerTelephoneNumber'] ?? null, + 'seller_nm' => $articleInfo['seller']['sellerName'] ?? null, + 'ownerTelNo' => $articleInfo['seller']['ownerTelephoneNumber'] ?? null, // 소유자전화번호 기존 없음 + 'ownerNm' => $articleInfo['seller']['ownerName'] ?? null, // 소유자명 기존 없음 + '_isOwnerCertificationAgree' => $articleInfo['seller']['isOwnerCertificationAgree'] ?? null, // 소유자확인동의여부 기존 없음 + 'direct_trad_yn' => $articleInfo['seller']['isDirectTrade'] ?? null, // 직거래여부 기존 없음 + 'realtor_nm' => $articleInfo['realtor']['realtorName'] ?? null, + 'realtor_tel_no' => $articleInfo['realtor']['representativeCellphoneNumber'] ?? null, + '_representativeTelephoneNumber' => $articleInfo['realtor']['representativeTelephoneNumber'] ?? null, // 중개사대표전화번호 기존 없음 + 'hscp_no' => $articleInfo['address']['complexNumber'] ?? null, + 'hscp_nm' => $articleInfo['address']['complexName'] ?? null, + 'ptp_no' => $articleInfo['address']['pyeongTypeNumber'] ?? null, + 'ptp_nm' => null, + 'charger' => null, + 'req_price_yn' => 'N', + 'reg_charger' => null, + 'dept1_sq' => null, + 'dept2_sq' => null, + 'reg_dept2_sq' => null, + 'reg_dept1_sq' => null, + 'dong_ho_chk' => $articleInfo['address']['isDongHoChecked'] ?? null, + 'hscplqry_lv' => $articleInfo['address']['inquiryLevel'] ?? 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, + 'roomSiteAtclExpsCnt' => null, + 'redvlp_area_nm' => $articleInfo['address']['redevelopmentArea']['redevelopmentAreaName'] ?? null, + 'biz_stp_desc' => $articleInfo['address']['redevelopmentArea']['businessStep'] ?? null, + 'cert_register' => empty($certRegister) ? null : json_encode($certRegister, JSON_UNESCAPED_UNICODE), + 'confirm_doc_img_url' => empty($confirm_doc_img_url) ? null : json_encode($confirm_doc_img_url, JSON_UNESCAPED_UNICODE), + 'confirm_doc_owner_check_yn' => $articleInfo['seller']['isOwnerCertificationAgree'] === true ? 'Y' : 'N', + 'owner_birth' => $articleInfo['seller']['ownerBirthDate'] ?? null, + 'vrfc_type_sub' => null, + 'cert_register_save_yn' => '', + 'confirm_doc_img_url_save_yn' => '', + 'reference_file_url' => empty($referenceFileUrl) ? null : json_encode($referenceFileUrl, JSON_UNESCAPED_UNICODE), + 'reference_file_url_save_yn' => '', + 'reference_file_url_yn' => empty($referenceFileUrl) ? 'N' : 'Y', + 'registerBookUniqueNo' => null, // 검증 참고란 + 'relationSellerAndOwner' => null, // 의뢰인과 소유주 관계 + 'ownerTypeCode' => getOwnerTypeCodeNo($articleInfo['seller']['ownerTypeCode']) ?? null, // 소유자구분코드 + 'registerBookUniqueNumber' => null, // 등기부 고유번호 + ]; + + + $article_info_etc_param = [ + 'corp_own' => $articleInfo['seller']['ownerTypeCode'] == 'CORP' ? 'Y' : 'N', + 'bild_no' => null, // 건물번호 + 'address2a' => $articleInfo['address']['liAddress'] ?? null, // 지번주소 + 'address2b' => $articleInfo['address']['jibunAddress'] ?? null, // 도로명주소 + 'expsStartYmdt' => $articleInfo['exposureStartDateTime'] ?? null, + 'vrfcAutoPassYn' => $articleInfo['isAutoVerificationRequested'] === true ? 'Y' : 'N', + 'registerBookUniqueNo' => null, + 'ownerTypeCode' => getOwnerTypeCodeNo($articleInfo['seller']['ownerTypeCode']) ?? null, + 'orgRepCphNo' => null, + 'orgRepTelNo' => null,// 원중개사 대표자명 + 'orgRltrNm' => null, // 원중개사 대표자명 + 'smsSendTime' => null, // 서류확인 내용 + 'document_cert_method' => null, + 'noRgbkVrfcReqYn' => $articleInfo['isUnregisteredVerificationRequested'] === true ? 'Y' : 'N', + 'areaByBdbkVrfcReqYn' => $articleInfo['isBuildingRegisterAreaCheckRequested'] === true ? 'Y' : 'N', //건축물대장기준 면적검증요청 여부 Y / N 또는 항목미전송 + 'orgAtclNo' => null, // 원매물 번호 + 'atclStatCd' => null, // 매물상태코드 J1W : 공동중개 검증 원)중개사 확인 요청 + 'repNm' => $articleInfo['realtor']['representativeName'] ?? null, // 원중개사 대표자명 + 'cpName' => $articleInfo['realtor']['realtorName'], // 중개업소 대표자명 ? + 'document_not_received' => null, + 'final_failure' => null + ]; + + $vrfc_params = array_merge($vrfc_params, $articl_info_param, $article_info_etc_param); + + // 개인: INDIV => 0 + // 법인: CORP => 1 + // 외국인: FRGNR => 2 + // 위임장: DELEG => 3 + return $vrfc_params; } + + + } diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php new file mode 100644 index 0000000..5ddd996 --- /dev/null +++ b/app/Services/StatusService.php @@ -0,0 +1,51 @@ +V2chgstatModel = model(V2chgstatModel::class); + $this->V2chghistoryModel = model(V2chghistoryModel::class); + } + + /** + * 상태 변경 및 이력 기록 공통 메서드 + */ + public function recordStatusAndHistory($vr_sq, $stat_cd, $chg_type, $memo) + { + // 1. 상태(stat) 저장 (Insert or Update) + try { + $this->V2chgstatModel->saveChgstat([ + 'vr_sq' => $vr_sq, + 'stat_cd' => $stat_cd, + 'insert_user' => '0', + 'insert_tm' => db_now() + ], 'I'); + } catch (\Exception $e) { + $lastSql = (string)$this->V2chgstatModel->getLastQuery(); + write_custom_log("STAT_SAVE_ERR | vr_sq: $vr_sq | SQL: $lastSql | Msg: " . $e->getMessage(), 'ERROR', 'failed'); + } + + // 2. 이력(history) 저장 + try { + $this->V2chghistoryModel->v2_savehistory([ + 'vr_sq' => $vr_sq, + 'stat_cd' => $stat_cd, + 'chg_type' => $chg_type, + 'memo' => $memo, + 'insert_id' => 'SYSTEM', + 'insert_tm' => db_now() + ]); + } catch (\Exception $e) { + $lastSql = (string)$this->V2chghistoryModel->getLastQuery(); + write_custom_log("HIST_SAVE_ERR | vr_sq: $vr_sq | SQL: $lastSql | Msg: " . $e->getMessage(), 'ERROR', 'failed'); + } + } +} \ No newline at end of file