db = \Config\Database::connect(); $this->naverClient = new NaverApiClient(); $this->VrfcReqModel = model(VrfcReqModel::class); $this->V2stdailyModel = model(V2stdailyModel::class); $this->rawStagingModel = model(NaverRawStagingModel::class); $this->receiptModel = model(ReceiptModel::class); $this->resultModel = model(ResultModel::class); $this->statusService = new StatusService(); // 인스턴스 생성 helper('log'); } /** * 메인 프로세스: 요청 타입에 따른 분기 처리 */ public function processArticle(array $payload) { $articleNumber = $payload['articleNumber']; $requestType = $payload['requestType'] ?? ''; // 1. 네이버 API 호출 $response = $this->naverClient->getArticleInfo($articleNumber); if (!$response || $response['code'] !== 'success') { throw new \Exception("네이버 API 응답 에러: $articleNumber"); } $rawData = $response['data']; $vType = $rawData['verificationTypeCode'] ?? ''; // 2. [Staging] 원본 DB 저장 (JSON 타입 컬럼 활용) $this->rawStagingModel->insert([ 'atcl_no' => $articleNumber, 'verification_type' => $vType, 'request_type' => $requestType, 'raw_json' => $rawData // 모델에서 json_encode 처리됨 ]); // 3. 타입별 분기 처리 if ($vType === 'S') { // [Type S] 현장확인 응답 처리 (A01 등) return $this->processTypeS($articleNumber, $rawData, $payload); } else { // [Type D/기타] 서류확인/비공동 처리 (D04, F01 등) return $this->processTypeV2($articleNumber, $rawData, $payload); } // $vrfcParams = $this->mapToDatabaseParams($response['data'], $payload); // write_custom_log("PROCESS_START | Type: $requestType | Atcl: $articleNumber", 'INFO', 'service'); // switch ($requestType) { // case 'REG': // 신규 등록 // $vr_sq = $this->insertVrfcReq($articleNumber, $vrfcParams); // if ($vr_sq) $this->V2stdailyModel->set_v2_st_daily(null, $vrfcParams['cpid'], $vrfcParams['vrfc_type'] . '0103', '1', 'add'); // break; // case 'MOD': // 수정 // $vr_sq = $this->updateVrfcReq($articleNumber, $vrfcParams); // if ($vr_sq) $this->V2stdailyModel->set_v2_st_daily(null, $vrfcParams['cpid'], $vrfcParams['vrfc_type'] . '0102', '1', 'add'); // break; // case 'CNC': // 취소 // $vr_sq = $this->deleteVrfcReq($articleNumber, $vrfcParams); // if ($vr_sq) $this->V2stdailyModel->set_v2_st_daily(null, $vrfcParams['cpid'], 'A0101', '1', 'add'); // 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"); // } // return ['vr_sq' => $vr_sq, 'articleNumber' => $articleNumber]; } /** * [Type S] 현장확인 응답 처리 (A01 등) */ private function processTypeS($articleNumber, $rawData, $payload) { $now = date('Y-m-d H:i:s'); // 시작 전 트랜잭션 $this->db->transStart(); try { // 1. receipt 데이터 준비 $receiptData = [ 'comp_sq' => '2', 'rcpt_rating' => '3', 'rcpt_key' => $rawData['cpArticleNumber'] ?? '', 'rcpt_atclno' => $articleNumber, 'rcpt_type' => 'C', 'rcpt_product' => $rawData['realEstateTypeCode'] ?? null, 'rcpt_product_nm' => $rawData['realEstateType'] ?? null, 'rcpt_product_info1'=> $rawData['tradeType'] ?? null, 'rcpt_product_info2'=> $rawData['price']['dealAmount'] ?? '0', 'rcpt_product_info3'=> $rawData['price']['leaseAmount'] ?? '0', 'rcpt_living_yn' => ($rawData['site']['isRegistration'] ?? false) ? 'Y' : 'N', 'rcpt_office' => $rawData['realtor']['realtorName'] ?? null, 'rcpt_agent' => $rawData['realtor']['realtorName'] ?? null, 'rcpt_sido' => mb_substr($rawData['address']['legalDivision']['cityNumber'] ?? '', 0, 5), 'rcpt_gugun' => mb_substr($rawData['address']['legalDivision']['divisionNumber'] ?? '', 0, 10), 'rcpt_dong' => $rawData['address']['legalDivision']['sectorNumber'] ?? null, 'rcpt_hscp_nm' => $rawData['address']['complexName'] ?? null, 'rcpt_hscp_no' => $rawData['address']['complexNumber'] ?? null, 'rcpt_dtl_addr' => trim(($rawData['address']['legalDivision']['legalDivisionAddress'] ?? '') . $rawData['address']['buildingName'] . '동 ' . ($rawData['address']['hoName'] ?? '') . '호'), 'rcpt_etc_addr' => $rawData['address']['hoName'] ?? null, 'rcpt_floor' => $rawData['floor']['correspondenceFloorCount'] ?? null, 'rcpt_floor2' => $rawData['floor']['totalFloorCount'] ?? null, 'rcpt_tm' => $now, 'rcpt_stat' => '100000', 'rcpt_x' => $rawData['address']['longitude'] ?? null, 'rcpt_y' => $rawData['address']['latitude'] ?? null, 'agent_id' => $rawData['realtor']['realtorName'] ?? null, 'agent_nm' => $rawData['realtor']['realtorName'] ?? null, 'agent_head_tel' => $rawData['realtor']['representativeCellphoneNumber'] ?? null, 'rsrv_date' => $rawData['site']['visitReserveDate'] ?? null, 'rsrv_tm_ap' => '00', // 컬럼명이 rsrv_tm_ap 인지 확인 필요 (제공해주신 스키마 기준) 'insert_tm' => $now, 'rcpt_cpid' => $rawData['cpId'] ?? 'naver', 'room_cnt' => $rawData['facilities']['roomCount'] ?? null, 'isSiteVRVerification' => ($rawData['site']['isVrVerification'] ?? false) ? 'Y' : 'N' ]; if (!$this->receiptModel->insert($receiptData)) { throw new \Exception("Receipt Insert 실패: " . json_encode($this->receiptModel->errors())); } $rcpt_sq = $this->receiptModel->getInsertID(); if ( $receiptData['isVrVerification'] == "Y") { $dept_sq = '29'; $usr_sq = '1993'; } // 2. result 데이터 준비 $resultData = [ 'rcpt_sq' => $rcpt_sq, 'use_yn' => 'Y', 'cust_nm' => '', 'rsrv_date' => $rawData['site']['visitReserveDate'] ?? null, 'rsrv_tm_ap' => '00', // 컬럼명이 rsrv_tm_ap 인지 확인 필요 (제공해주신 스키마 기준) 'result_cd1' => '10', 'result_cd2' => '1000', 'result_cd3' => '100000', 'insert_tm' => $now, 'insert_usr' => 0, 'update_tm' => $now, 'update_usr' => 0, 'dept_sq' => $dept_sq, // 필요 시 매핑 로직 추가 'usr_sq' => $usr_sq, // 필요 시 매핑 로직 추가 'resYn' => ($rawData['verificationResult']['isSuccessful'] ?? false) ? 'Y' : 'N', ]; if (!$this->resultModel->insert($resultData)) { throw new \Exception("Result Insert 실패"); } $this->db->transComplete(); // transComplete 이후에 transStatus를 확인하는 것이 CI4의 표준입니다. if ($this->db->transStatus() === false) { // transComplete가 실패하면 자동으로 롤백되지만, 명시적 예외 처리가 안전합니다. throw new \Exception("Type S DB 트랜잭션 최종 실패"); } return $rcpt_sq; } catch (\Exception $e) { // 이미 transComplete 내부에서 실패 시 롤백되지만, 예외 발생 시 수동 롤백 보장 if ($this->db->transEnabled()) { $this->db->transRollback(); } throw $e; } } /** * [REG] 신규 등록 */ private function insertVrfcReq($articleNumber, $params) { CLI::write(CLI::color('🟢 매물 정보 시작', 'green')); $existing = $this->VrfcReqModel->where('atcl_no', $articleNumber)->first(); if ($existing) throw new \Exception("중복 등록 시도: $articleNumber"); $params['stat_cd'] = '10'; $params['insert_user'] = '0'; $params['req_type'] = 'C'; if (!$this->VrfcReqModel->insert($params)) { $sql = (string)$this->VrfcReqModel->getLastQuery(); write_custom_log("INSERT_FAILED | Atcl: $articleNumber | SQL: $sql", 'ERROR', 'failed'); throw new \Exception("신규 등록 실패"); } $vr_sq = $this->VrfcReqModel->getInsertID(); $this->statusService->recordStatusAndHistory($vr_sq, '10', 'C9', "신규접수 : 10"); return $vr_sq; } /** * [MOD] 수정 처리 */ private function updateVrfcReq($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'; $params['insert_tm'] = db_now(); return $this->updateProcess($existing, $params, 'MOD', "재접수 상태변경: {$existing['stat_cd']} => 30"); } /** * [CNC] 취소 처리 */ private function deleteVrfcReq($articleNumber, $params) { 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'; return $this->updateProcess($existing, $params, 'CNC', "취소 처리: {$existing['stat_cd']} => 19"); } /** * [FIN] 완료 처리 */ private function finVrfcReq($articleNumber, $params) { 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'; return $this->updateProcess($existing, $params, 'FIN', "완료 처리: {$existing['stat_cd']} => 60"); } // --- 내부 공통 유틸리티 함수 --- private function findExisting($articleNumber) { $existing = $this->VrfcReqModel->where('atcl_no', $articleNumber)->first(); if (!$existing) throw new \Exception("해당 매물 없음: $articleNumber 재등록 필요"); return $existing; } /** * 공통 업데이트 및 이력 기록 로직 (Lock 최소화) */ private function updateProcess($existing, $params, $type, $memo) { $vr_sq = $existing['vr_sq']; if (!$this->VrfcReqModel->update($vr_sq, $params)) { $sql = (string)$this->VrfcReqModel->getLastQuery(); write_custom_log("UPDATE_FAILED | Type: $type | vr_sq: $vr_sq | SQL: $sql", 'ERROR', 'failed'); throw new \Exception("[$type] 업데이트 실패"); } $this->statusService->recordStatusAndHistory($vr_sq, $params['stat_cd'], 'C9', $memo); return $vr_sq; } /** * API 데이터를 DB 컬럼에 맞게 변환 */ private function mapToDatabaseParams(array $articleInfo, array $payload): array { $files = $articleInfo['files'] ?? []; $certRegister = []; $confirm_doc_img_url = []; $referenceFileUrl = []; $requestDatetime = date('YmdHis', strtotime($payload['requestDatetime'] ?? 'now')); 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']; } } $ownerTypeRaw = $articleInfo['seller']['ownerTypeCode'] ?? null; $vrfc_params = [ 'reqSeq' => '', 'atcl_no' => $articleInfo['articleNumber'], 'step' => '', 'cpid' => $articleInfo['cpId'], 'cp_atcl_id' => $articleInfo['cpArticleNumber'], 'trade_type' => $articleInfo['tradeTypeCode'], 'realtor_nm' => $articleInfo['realtor']['realtorName'], 'realtor_tel_no' => $articleInfo['realtor']['representativeCellphoneNumber'], 'seller_tel_no' => $articleInfo['seller']['sellerTelephoneNumber'], 'vrfc_type' => $articleInfo['verificationTypeCode'], 'rgbk_confirm' => $articleInfo['isUnregisteredVerificationRequested'] ? 'Y' : 'N', 'req_type' => '', 'rdate' => $requestDatetime ?? db_now('Y-m-d H:i:s'), 'cpTelNo' => $articleInfo['seller']['sellerTelephoneNumber'], 'stat_cd' => '', 'try_cnt' => '0', 'insert_user' => '', 'insert_tm' => db_now(), 'memo' => '', 'contact_fail_cnt' => '0', 'sync_yn' => 'Y', 'reg_try_cnt' => '0', 'tel_fail_cause' => null, 'rgbk_confirm_owner_nm' => $articleInfo['seller']['ownerName'] ?? null, 'direct_trad_yn' => $articleInfo['seller']['isDirectTrade'] === true ? 'Y' : 'N', 'confirm_doc_img_url' => empty($confirm_doc_img_url) ? null : json_encode($confirm_doc_img_url, JSON_UNESCAPED_UNICODE), 'confirm_doc_owner_check_yn' => '', 'owner_verifiable' => null, 'vrfc_cmpl_type' => null, 'rgbk_doc_img_url' => null, 'certRegister' => empty($certRegister) ? null : json_encode($certRegister, JSON_UNESCAPED_UNICODE), '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($ownerTypeRaw) ?? null, // 소유자구분코드 'registerBookUniqueNumber' => null, // 등기부 고유번호 ]; $article_info_etc_param = [ 'corp_own' => $ownerTypeRaw == '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($ownerTypeRaw) ?? 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; } }