Files
confirms/app/Services/NaverService.php
2026-01-19 17:44:05 +09:00

494 lines
23 KiB
PHP

<?php
namespace App\Services;
use CodeIgniter\CLI\CLI;
use App\Libraries\NaverApiClient;
use App\Models\Entities\VrfcReqModel;
use App\Models\Entities\V2stdailyModel;
use App\Models\Entities\NaverRawStagingModel;
use App\Models\Entities\ReceiptModel;
use App\Models\Entities\ResultModel;
use App\Services\StatusService; // 추가
use Exception;
class NaverService
{
protected $db;
protected $naverClient, $VrfcReqModel, $V2stdailyModel, $statusService, $rawStagingModel, $receiptModel, $resultModel;
public function __construct()
{
$this->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;
}
}