Compare commits
37 Commits
761607beae
...
36b06a66d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b06a66d2 | ||
| 438f0a546c | |||
| 1e5e44e10b | |||
| c129a277b0 | |||
| c49a7e0a32 | |||
| b971005525 | |||
| da33e34d4f | |||
| 8338df57c9 | |||
| 094fa7c640 | |||
|
|
7627951c09 | ||
|
|
cfd2ee2787 | ||
| 546d23f077 | |||
| 249efb0a29 | |||
| dbeb0d6b1f | |||
| 4cfd6f1faf | |||
| e085eccaab | |||
| b42755bc27 | |||
| 22fa8b32a1 | |||
| 318f2c063e | |||
| e377dee571 | |||
| bbe47ad097 | |||
| ac75b7ebf5 | |||
| b03f783051 | |||
| 24b0548002 | |||
| 946bc15aa6 | |||
| 76e79ea4cf | |||
| 454bb77a07 | |||
| 8864a46c8a | |||
| 1d693df861 | |||
| 6576b59d7e | |||
| c30b30bb07 | |||
| 0e9915bd7a | |||
| 11840905f1 | |||
| 98c36e13f6 | |||
| e4c7b633ce | |||
| a8b66af2fa | |||
| dbc9d875c3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -171,3 +171,4 @@ _modules/*
|
||||
/dist/
|
||||
/node_modules/
|
||||
.env
|
||||
**/logs/
|
||||
@@ -5,103 +5,81 @@ namespace App\Commands;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
use App\Models\Entities\NaverWorkerLogModel; // 새로 만든 테이블용 모델
|
||||
|
||||
// 헬퍼 로드 (app/Helpers/log_helper.php 가 있어야 함 autoload 설정 넣어놓았음)
|
||||
|
||||
class NaverWorker extends BaseCommand
|
||||
{
|
||||
protected $group = 'Workers';
|
||||
protected $name = 'naver:worker';
|
||||
protected $description = 'Process Naver verification requests from Redis queue with retry logic.';
|
||||
|
||||
// 최대 재시도 횟수 정의
|
||||
private const MAX_RETRIES = 3;
|
||||
private const RETRY_DELAY = 60; // 초 단위 (60초 지연 후 재시도)
|
||||
protected $description = 'Redis에서 데이터를 꺼내 DB에 저장하고 네이버 API를 호출합니다.';
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
helper('log'); // 여기서 로드 완료!
|
||||
|
||||
$logModel = model(NaverWorkerLogModel::class);
|
||||
$naverService = new \App\Services\NaverService(); // 서비스 생성
|
||||
|
||||
$redis = new \Redis();
|
||||
// 환경 변수를 사용하도록 변경 (php_worker service의 env 설정 활용)
|
||||
$redisHost = getenv('REDIS_HOST') ?: 'redis';
|
||||
$redisPort = getenv('REDIS_PORT') ?: 6379;
|
||||
|
||||
$redis->connect($redisHost, $redisPort);
|
||||
|
||||
CLI::write('Worker started. Listening on naver:queue...');
|
||||
|
||||
while (true) {
|
||||
// 메인 큐 및 재시도 큐에서 데이터 꺼내기 (Blocking Pop, 5초 타임아웃)
|
||||
// LIFO (lPush/brPop) 사용 시: ['naver:queue:retry', 'naver:queue']
|
||||
$item = $redis->brPop(['naver:worker_queue'], 5);
|
||||
|
||||
if ($item) {
|
||||
$payload = json_decode($item[1], true);
|
||||
$this->process($redis, $payload); // Redis 객체를 process에 전달
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function process(\Redis $redis, $payload)
|
||||
{
|
||||
$articleNum = $payload['articleNumbr'];
|
||||
$requestType = $payload['reqeustType'];
|
||||
$retryCount = $payload['retry_count'] ?? 0;
|
||||
|
||||
CLI::write("Processing {$requestType} for {$articleNum} (Attempt: " . ($retryCount + 1) . ")");
|
||||
|
||||
if (!in_array($requestType, ['REG', 'MOD'])) {
|
||||
CLI::write("Skipping non-verification request {$requestType} for {$articleNum}");
|
||||
try {
|
||||
$redis->connect('redis', 6379);
|
||||
$redis->select(9);
|
||||
CLI::write(CLI::color('🟢 Naver Worker running...', 'green'));
|
||||
} catch (\Exception $e) {
|
||||
CLI::error("Redis 연결 불가: " . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 네이버 CP API 호출
|
||||
$client = \Config\Services::curlrequest([
|
||||
'timeout' => 10,
|
||||
// ... (인증 헤더 설정 등 이전 답변의 보안 관련 항목 추가)
|
||||
]);
|
||||
$url = "https://네이버CP/kiso/center/verification-article/{$articleNum}";
|
||||
|
||||
|
||||
try {
|
||||
$response = $client->get($url);
|
||||
$statusCode = $response->getStatusCode();
|
||||
while (true) {
|
||||
$result = $redis->brPop(['naver:raw_queue'], 30);
|
||||
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
// 2. 성공 처리
|
||||
CLI::write("✅ SUCCESS: {$requestType} for {$articleNum} (Status: {$statusCode})");
|
||||
// 성공 시 DB에 결과 업데이트 등 후속 작업 진행
|
||||
} else {
|
||||
// 3. API 실패 (4xx, 5xx 에러)
|
||||
$this->handleFailure($redis, $payload, $retryCount, "API returned Status: {$statusCode}");
|
||||
if (!$result) {
|
||||
// 데이터가 없어서 타임아웃 난 경우.
|
||||
// 굳이 sleep 안 해도 바로 다음 brPop이 다시 30초 대기를 시작함.
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
$rawData = $result[1];
|
||||
// [1] 꺼내자마자 DB에 원문 저장 (2차 임시 저장)
|
||||
$logId = $logModel->insert([
|
||||
'raw_payload' => $rawData,
|
||||
'status' => 'INIT'
|
||||
]);
|
||||
|
||||
try {
|
||||
$responseJson = json_decode($result[1], true);
|
||||
$payload = $responseJson['request_data'] ?? [];
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new \Exception("빈 페이로드 데이터");
|
||||
}
|
||||
|
||||
// 서비스의 함수 하나로 모든 처리 완료
|
||||
$insertId = $naverService->processArticle($payload);
|
||||
|
||||
// [3] 성공 시 로그 업데이트
|
||||
$logModel->update($logId, [
|
||||
'atcl_no' => $payload['articleNumber'] ?? null,
|
||||
'status' => 'SUCCESS',
|
||||
'target_db_id' => $insertId
|
||||
]);
|
||||
|
||||
CLI::write("✅ Success! DB ID: $insertId", 'cyan');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
CLI::error("❌ Task Failed: " . $e->getMessage());
|
||||
// 실패 로그는 여기서 남김
|
||||
helper('log');
|
||||
write_custom_log("FAILED_DATA | Error: " . $e->getMessage(), 'ERROR', 'failed');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 4. 네트워크/타임아웃 오류
|
||||
$this->handleFailure($redis, $payload, $retryCount, "Network Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function handleFailure(\Redis $redis, $payload, $retryCount, $reason)
|
||||
{
|
||||
$articleNum = $payload['articleNumbr'];
|
||||
|
||||
if ($retryCount < self::MAX_RETRIES) {
|
||||
// 5. 재시도 로직: 재시도 횟수 증가 및 큐에 다시 푸시
|
||||
$payload['retry_count'] = $retryCount + 1;
|
||||
|
||||
// 지연된 재시도를 위해 Sorted Set (ZADD) 또는 별도의 큐 사용을 고려할 수 있으나,
|
||||
// 여기서는 단순성을 위해 즉시 재시도 큐에 넣고 sleep을 사용하는 방식을 가정합니다.
|
||||
// **주의: 실제 프로덕션 환경에서는 `Redis ZSET`을 사용하여 지연된 작업을 처리하는 것이 더 일반적입니다.**
|
||||
|
||||
// 단순 재시도 큐 (ZSET을 사용하지 않는 경우)
|
||||
$redis->lPush('naver:queue:retry', json_encode($payload));
|
||||
|
||||
// CLI 환경에서 재시도 간 지연 시간을 강제 (Blocking pop을 쓰므로 실제 지연은 다음 worker 실행 시 발생)
|
||||
CLI::error("Attempt {$retryCount} FAILED for {$articleNum}. Reason: {$reason}. Retrying later.");
|
||||
|
||||
} else {
|
||||
// 6. 최종 실패 처리: DLQ로 이동
|
||||
$payload['fail_reason'] = $reason;
|
||||
$payload['fail_time'] = date('Y-m-d H:i:s');
|
||||
|
||||
$redis->lPush('naver:queue:dlq', json_encode($payload));
|
||||
|
||||
CLI::error("❌ CRITICAL FAIL: {$articleNum} moved to DLQ after " . self::MAX_RETRIES . " attempts.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,5 +88,5 @@ class Autoload extends AutoloadConfig
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public $helpers = ['url'];
|
||||
public $helpers = ['log','url', 'function'];
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class Cache extends BaseConfig
|
||||
// Redis 설정에 .env 값을 할당 (이전 논의된 Docker 호스트 이름 'redis' 사용)
|
||||
$this->redis = [
|
||||
'host' => env('redis.default.host', '127.0.0.1'),
|
||||
'password' => env('redis.default.password', null),
|
||||
'password' => (env('redis.default.password') === '' || env('redis.default.password') === null) ? null : env('redis.default.password'),
|
||||
'port' => (int)env('redis.default.port', 6379),
|
||||
'timeout' => (int)env('redis.default.timeout', 0),
|
||||
'database' => (int)env('redis.default.database', 0)
|
||||
|
||||
@@ -16,8 +16,8 @@ $routes->get('/logout', 'Login::out');
|
||||
$routes->get('/', 'Home\Home::dashboard');
|
||||
$routes->get('/home', 'Home\Home::dashboard');
|
||||
|
||||
$routes->get('/home/viewStatData', to: 'Home\Home::viewStatData'); // 실적조회
|
||||
$routes->get('/home/getHomeFaxCount', to: 'Home\Home::getHomeFaxCount'); // 팩스조회
|
||||
$routes->get('/home/viewStatData', 'Home\Home::viewStatData'); // 실적조회
|
||||
$routes->get('/home/getHomeFaxCount', 'Home\Home::getHomeFaxCount'); // 팩스조회
|
||||
|
||||
/**
|
||||
* 공통 API
|
||||
@@ -143,6 +143,12 @@ $routes->group('', ['namespace' => 'App\Controllers\V2'], static function ($rout
|
||||
$routes->get('m705a/getResultList', 'M705::getResultList');
|
||||
$routes->get('m705a/excel', 'M705::excel');
|
||||
|
||||
$routes->post('m705a/rotateImage', 'M705::rotateImage'); // 이미지 회전
|
||||
$routes->post('m705a/saveCorp', 'M705::saveCorp'); // 법인저장
|
||||
$routes->post('m705a/uploadFile', 'M705::uploadFile'); // 파일업로드
|
||||
|
||||
$routes->post('m705a/getNextInfo', 'M705::getNextInfo'); // 다음매물확인
|
||||
$routes->post('m705a/nextRegi', 'M705::saveRegi'); // 매물저장
|
||||
|
||||
|
||||
});
|
||||
@@ -227,11 +233,11 @@ $routes->group('article', ['namespace' => 'App\Controllers\Article'], function (
|
||||
*/
|
||||
$routes->group('results', ['namespace' => 'App\Controllers\Results'], function ($routes) {
|
||||
/** 화면 */
|
||||
$routes->match(['get', 'post'], 'summary/stats_s01', 'Summary::lists'); // 현장확인요약실적
|
||||
$routes->match(['get', 'post'], 'dept/stats_d01', 'Dept::lists'); // 현장확인요약실적
|
||||
$routes->match(['get', 'post'], 'person/stats_p01', 'Person::lists'); // 현장확인개인별실적
|
||||
$routes->match(['get', 'post'], 'assign/stats_a01', 'Assign::lists'); // 현장확인인원별배정현황
|
||||
$routes->match(['get', 'post'], 'm409/m409a/stats', 'M409::stats'); // 확인매물일별실적
|
||||
$routes->match(['GET', 'POST'], 'summary/stats_s01', 'Summary::lists'); // 현장확인요약실적
|
||||
$routes->match(['GET', 'POST'], 'dept/stats_d01', 'Dept::lists'); // 현장확인요약실적
|
||||
$routes->match(['GET', 'POST'], 'person/stats_p01', 'Person::lists'); // 현장확인개인별실적
|
||||
$routes->match(['GET', 'POST'], 'assign/stats_a01', 'Assign::lists'); // 현장확인인원별배정현황
|
||||
$routes->match(['GET', 'POST'], 'm409/m409a/stats', 'M409::stats'); // 확인매물일별실적
|
||||
|
||||
|
||||
/** API - 현장확인조직별실적 */
|
||||
@@ -256,7 +262,7 @@ $routes->group('', ['namespace' => 'App\Controllers\Results'], static function (
|
||||
|
||||
// 확인매물일별실적
|
||||
$routes->group('m409', static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'm409a/stats', 'M409::stats');
|
||||
$routes->match(['GET', 'POST'], 'm409a/stats', 'M409::stats');
|
||||
|
||||
// API
|
||||
$routes->get('m409a/getResultList', 'M409::getResultList');
|
||||
@@ -265,7 +271,7 @@ $routes->group('', ['namespace' => 'App\Controllers\Results'], static function (
|
||||
|
||||
// 확인매물개인별실적
|
||||
$routes->group('m410', static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'm410a/stats', 'M410::stats');
|
||||
$routes->match(['GET', 'POST'], 'm410a/stats', 'M410::stats');
|
||||
|
||||
// API
|
||||
$routes->get('m410a/getResultList', 'M410::getResultList');
|
||||
@@ -274,7 +280,7 @@ $routes->group('', ['namespace' => 'App\Controllers\Results'], static function (
|
||||
|
||||
// 확인매물매체사실적
|
||||
$routes->group('m411', static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'm411a/stats', 'M411::stats');
|
||||
$routes->match(['GET', 'POST'], 'm411a/stats', 'M411::stats');
|
||||
|
||||
// API
|
||||
$routes->get('m411a/getResultList', 'M411::getResultList');
|
||||
@@ -283,7 +289,7 @@ $routes->group('', ['namespace' => 'App\Controllers\Results'], static function (
|
||||
|
||||
// 확인매물일자별실적
|
||||
$routes->group('m412', static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'm412a/stats', 'M412::stats');
|
||||
$routes->match(['GET', 'POST'], 'm412a/stats', 'M412::stats');
|
||||
|
||||
// API
|
||||
$routes->get('m412a/getResultList', 'M412::getResultList');
|
||||
@@ -293,7 +299,7 @@ $routes->group('', ['namespace' => 'App\Controllers\Results'], static function (
|
||||
|
||||
// 검증소요시간
|
||||
$routes->group('m415', static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'm415a/stats', 'M415::stats');
|
||||
$routes->match(['GET', 'POST'], 'm415a/stats', 'M415::stats');
|
||||
|
||||
// API
|
||||
$routes->get('m415a/getResultList', 'M415::getResultList');
|
||||
@@ -302,7 +308,7 @@ $routes->group('', ['namespace' => 'App\Controllers\Results'], static function (
|
||||
|
||||
// 개인별이동거리
|
||||
$routes->group('m416', static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'm416a/stats', 'M416::stats');
|
||||
$routes->match(['GET', 'POST'], 'm416a/stats', 'M416::stats');
|
||||
|
||||
// API
|
||||
$routes->get('m416a/getResultList', 'M416::getResultList');
|
||||
@@ -311,7 +317,7 @@ $routes->group('', ['namespace' => 'App\Controllers\Results'], static function (
|
||||
|
||||
// 신규매물실적관리
|
||||
$routes->group('m417', static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'm417a/stats', 'M417::stats');
|
||||
$routes->match(['GET', 'POST'], 'm417a/stats', 'M417::stats');
|
||||
|
||||
// API
|
||||
$routes->get('m417a/getResultList', 'M417::getResultList');
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
namespace App\Controllers\V2;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\Common;
|
||||
use App\Libraries\MyUpload;
|
||||
use App\Models\common\CodeModel;
|
||||
use App\Models\v2\M705Model;
|
||||
|
||||
@@ -144,4 +146,205 @@ class M705 extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 상세화면
|
||||
public function detail($id)
|
||||
{
|
||||
$id = (int) $id;
|
||||
|
||||
if ($id <= 0) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$codes = $this->codeModel->getCodeLists(['VRFCREQ_WAY', 'CONFIRM_RESULT_D11', 'CONFIRM_RESULT_T11', 'TRADE_TYPE', 'CERT_UNCNFRM_STATUS']); // 코드조회
|
||||
|
||||
$data = $this->model->getDetail($id);
|
||||
$record = $this->model->getRecordInfo($id, '1'); // 홍보확인서
|
||||
$regist = $this->model->getRecordInfo($id, '2'); // 등기부등본
|
||||
$memo = $this->model->getMemo($id); // 메모
|
||||
$display = $this->model->getDisplay('M705_detail');
|
||||
$reference = $this->model->getAllRecordInfo($id, '7'); //참고용파일 (2017.09.26 추가)
|
||||
|
||||
$this->data['codes'] = $codes;
|
||||
$this->data['data'] = $data;
|
||||
$this->data['record'] = $record;
|
||||
$this->data['regist'] = $regist;
|
||||
$this->data['memo'] = $memo;
|
||||
$this->data['display'] = $display;
|
||||
$this->data['reference'] = $reference;
|
||||
|
||||
return view("pages/v2/m705/detail", $this->data);
|
||||
}
|
||||
|
||||
// 이미지회전
|
||||
public function rotateImage()
|
||||
{
|
||||
$common = new Common();
|
||||
|
||||
try {
|
||||
|
||||
$vr_sq = $this->request->getPost('vr_sq');
|
||||
$degress = $this->request->getPost('degress');
|
||||
|
||||
if (empty($degrees) || !is_numeric($degrees)) {
|
||||
$degrees = 90;
|
||||
}
|
||||
|
||||
$regist = $this->model->getRecordInfo($vr_sq, '2');
|
||||
$fullPath = $regist['file_path'] . $regist['file_name'];
|
||||
$fullPath = $_SERVER['DOCUMENT_ROOT'] . $common->realpath_to_webpath($fullPath);
|
||||
|
||||
$degrees = (float) $degrees;
|
||||
|
||||
$im = new \Imagick($fullPath);
|
||||
|
||||
// 배경색(회전 시 빈 공간 채우는 색). 투명 원하면 'transparent'
|
||||
$im->setImageBackgroundColor(new \ImagickPixel('white'));
|
||||
|
||||
// 회전
|
||||
$im->rotateImage($im->getImageBackgroundColor(), $degrees);
|
||||
|
||||
// 포맷/압축 유지(옵션)
|
||||
$im->setImageCompressionQuality(90);
|
||||
|
||||
// 덮어쓰기
|
||||
$im->writeImage($fullPath);
|
||||
|
||||
$im->clear();
|
||||
$im->destroy();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'code' => '0',
|
||||
'msg' => 'success',
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->response->setJSON([
|
||||
'code' => '9',
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 법인저장
|
||||
public function saveCorp()
|
||||
{
|
||||
try {
|
||||
|
||||
$vr_sq = $this->request->getPost('vr_sq');
|
||||
$atcl_no = $this->request->getPost('atcl_no');
|
||||
|
||||
$this->model->saveCorp($vr_sq, $atcl_no);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'code' => '0',
|
||||
'msg' => 'success',
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->response->setJSON([
|
||||
'code' => '9',
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 파일업로드
|
||||
public function uploadFile()
|
||||
{
|
||||
$lib = new MyUpload();
|
||||
|
||||
try {
|
||||
$usr_id = session('usr_id');
|
||||
$vr_sq = $this->request->getPost('vr_sq');
|
||||
|
||||
$file = $this->request->getFile('file');
|
||||
|
||||
if ($file && $file->isValid() && !$file->hasMoved()) {
|
||||
|
||||
$uploadPath = "/upload/v2_file/" . $vr_sq . "/";
|
||||
|
||||
$arrUploadfile = [];
|
||||
if ($file->isValid() && !$file->hasMoved()) {
|
||||
$uploadData = $lib->do_upload2($file, $uploadPath);
|
||||
|
||||
if ($uploadData !== false) {
|
||||
$arrUploadfile[] = $uploadData;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($arrUploadfile)) {
|
||||
foreach ($arrUploadfile as $key => $uploadFile) {
|
||||
$data = [
|
||||
'vr_sq' => $vr_sq,
|
||||
// 'file_sq' => $this->request->getPost('file_sq'),
|
||||
'orig_name' => $uploadFile['origin_name'],
|
||||
'new_name' => $uploadFile['file_name'],
|
||||
'file_path' => $uploadPath, // 필요에 따라 상대경로로만 저장
|
||||
'ext' => '.' . $uploadFile['ext'],
|
||||
'size' => $file->getSize(),
|
||||
'img_yn' => null,
|
||||
'img_height' => null,
|
||||
'img_width' => null,
|
||||
'usr_id' => $usr_id,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($data)) {
|
||||
|
||||
// 파일업로드 정보 저장
|
||||
$this->model->saveFileInfo($data);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return $this->response->setJSON([
|
||||
'code' => '0',
|
||||
'msg' => 'success'
|
||||
]);
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->response->setJSON([
|
||||
'code' => '9',
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 다음매물 확인
|
||||
public function getNextInfo()
|
||||
{
|
||||
try {
|
||||
|
||||
$vr_sq = $this->request->getPost('vr_sq');
|
||||
|
||||
$data = $this->model->getNextInfo($vr_sq);
|
||||
|
||||
if (empty($data)) {
|
||||
return $this->response->setJSON([
|
||||
'code' => '9',
|
||||
'msg' => '등기부등본 이미지가 존재하지 않습니다.'
|
||||
]);
|
||||
} else {
|
||||
return $this->response->setJSON([
|
||||
'code' => '0',
|
||||
'msg' => 'success',
|
||||
'resw' => $data['vr_sq']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->response->setJSON([
|
||||
'code' => '9',
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,6 +237,22 @@ function han($s)
|
||||
}
|
||||
// function to_han ($str) { return preg_replace('/(\\\u[a-f0-9]+)+/e','han("$0")',$str); }
|
||||
|
||||
if (!function_exists('db_now')) {
|
||||
/**
|
||||
* DB의 현재 시간을 지정된 포맷으로 반환하는 RawSql 생성
|
||||
* @param string|null $format MariaDB 포맷 (예: '%Y-%m-%d %H:%i:%s')
|
||||
*/
|
||||
function db_now(?string $format = null)
|
||||
{
|
||||
if ($format) {
|
||||
// 포맷이 있으면 DATE_FORMAT(NOW(), '포맷') 형태로 생성
|
||||
return new \CodeIgniter\Database\RawSql("DATE_FORMAT(NOW(), '$format')");
|
||||
}
|
||||
// 포맷이 없으면 기본 NOW() 반환
|
||||
return new \CodeIgniter\Database\RawSql('NOW()');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 문자 조합 검사
|
||||
* - 영문 대문자 / 소문자 / 숫자 / 특수문자 중 최소 $minTypes 종류 이상
|
||||
|
||||
45
app/Helpers/log_helper.php
Normal file
45
app/Helpers/log_helper.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('write_custom_log')) {
|
||||
/**
|
||||
* 전용 로그 기록 함수 (Worker, API 리시버 등 어디서나 사용 가능)
|
||||
*/
|
||||
function write_custom_log($message, $level = 'INFO', $type = 'service')
|
||||
{
|
||||
$logDir = WRITEPATH . 'logs/worker';
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0777, true);
|
||||
}
|
||||
|
||||
// --- 호출 위치 추적 로직 추가 ---
|
||||
// debug_backtrace는 호출 스택을 가져옵니다.
|
||||
// [0]은 현재 함수(write_custom_log), [1]은 이 함수를 호출한 곳입니다.
|
||||
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
||||
$caller = $bt[1] ?? null;
|
||||
$fileInfo = $bt[0] ?? null; // 파일명과 라인수는 호출 시점인 0번 인덱스에 들어있음
|
||||
|
||||
$location = 'unknown';
|
||||
if ($caller) {
|
||||
$class = $caller['class'] ?? '';
|
||||
$func = $caller['function'] ?? '';
|
||||
$line = $fileInfo['line'] ?? '0';
|
||||
|
||||
// 클래스명에서 Namespace 제외하고 클래스명만 짧게 가져오기 (선택 사항)
|
||||
$classShort = substr(strrchr($class, "\\"), 1) ?: $class;
|
||||
|
||||
$location = "{$classShort}::{$func}:{$line}";
|
||||
}
|
||||
// ----------------------------
|
||||
|
||||
$suffix = ($type === 'failed') ? '_failed' : '';
|
||||
$logFile = $logDir . '/' . date('Y-m-d') . $suffix . '.log';
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$singleLine = str_replace(["\r", "\n", "\t"], " ", $message);
|
||||
|
||||
// 포맷에 [$location] 추가
|
||||
$formatted = "[$timestamp] [$level] [$location] $singleLine" . PHP_EOL;
|
||||
|
||||
@file_put_contents($logFile, $formatted, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,27 @@ class Common
|
||||
return $pagination;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 서버상의 위치를 웹상의 위치로 변경한다...
|
||||
*/
|
||||
public function realpath_to_webpath($realpath)
|
||||
{
|
||||
$arrImagePath = array(
|
||||
'/home/confirms/test-admin.confirms.co.kr/upload/',
|
||||
'/home/confirms/upload/',
|
||||
'/home/www/admin.confirms.co.kr/upload/',
|
||||
'/home/www/upload/',
|
||||
'/image/confirms_upload/',
|
||||
'/misc/image/confirms_upload/',
|
||||
'/storage/web/admin.confirms.co.kr/src/upload/',
|
||||
'/storage/web/admin.confirms.co.kr/upload/',
|
||||
$_SERVER['DOCUMENT_ROOT'] . '/upload/',
|
||||
);
|
||||
|
||||
$return_path = str_replace($arrImagePath, '/upload/', $realpath);
|
||||
$return_path = str_replace(' ', '', $return_path);
|
||||
return $return_path;
|
||||
}
|
||||
|
||||
}
|
||||
91
app/Libraries/NaverApiClient.php
Normal file
91
app/Libraries/NaverApiClient.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
class NaverApiClient
|
||||
{
|
||||
protected $baseUrl = 'https://test-b2b.land.naver.com';
|
||||
protected $charger = '';
|
||||
|
||||
/**
|
||||
* [GET] 매물 정보 조회
|
||||
*/
|
||||
public function getArticleInfo(string $articleNumber): ?array
|
||||
{
|
||||
$this->charger = 'admin';
|
||||
$url = "{$this->baseUrl}/kiso/center/verification-article/{$articleNumber}?charger={$this->charger}";
|
||||
|
||||
return $this->request('GET', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* [PUT] 매물 정보 수정
|
||||
* @param string $articleNumber 매물번호
|
||||
* @param array $updateData 수정할 데이터 (tradeType, price, space 등)
|
||||
*/
|
||||
public function updateArticleInfo(string $articleNumber, array $updateData, string $charger = 'admin'): ?array
|
||||
{
|
||||
$this->charger = $charger;
|
||||
$url = "{$this->baseUrl}/kiso/center/verification-article/{$articleNumber}?charger={$this->charger}";
|
||||
|
||||
return $this->request('PUT', $url, $updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* CURL 공통 실행 함수
|
||||
*/
|
||||
private function request(string $method, string $url, ?array $data = null): ?array
|
||||
{
|
||||
/**
|
||||
* curl --location 'https://test-b2b.land.naver.com/kiso/center/verification-article/2500000001?charger=admin' \
|
||||
--header 'X-Naver-Client-Id: yqBbvQZ123_hjH3b3Df9' \
|
||||
*/
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
if ($method === 'PUT') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||
if ($data) {
|
||||
$payload = json_encode($data);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($payload),
|
||||
'X-Naver-Client-Id: yqBbvQZ123_hjH3b3Df9'
|
||||
]);
|
||||
}
|
||||
} elseif ($method === 'GET') {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'X-Naver-Client-Id: yqBbvQZ123_hjH3b3Df9'
|
||||
]);
|
||||
} elseif ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if ($data) {
|
||||
$payload = json_encode($data);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($payload),
|
||||
'X-Naver-Client-Id: yqBbvQZ123_hjH3b3Df9'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// 결과 로그 기록 (성공/실패 모두 기록하여 추적 가능하게 함)
|
||||
if ($httpCode === 200 && $httpdCode === 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");
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
}
|
||||
26
app/Models/Entities/NaverWorkerLogModel.php
Normal file
26
app/Models/Entities/NaverWorkerLogModel.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class NaverWorkerLogModel extends Model
|
||||
{
|
||||
protected $table = 'naver_worker_logs';
|
||||
protected $primaryKey = 'seq'; // 'id'가 아니므로 명시 필요
|
||||
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $returnType = 'array'; // 또는 'object'
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
// 대량 입력을 허용할 필드들
|
||||
protected $allowedFields = [
|
||||
'atcl_no', 'raw_payload', 'status',
|
||||
'retry_cnt', 'error_msg', 'target_db_id'
|
||||
];
|
||||
|
||||
// 날짜 자동 업데이트 설정
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
97
app/Models/Entities/ReceiptModel.php
Normal file
97
app/Models/Entities/ReceiptModel.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ReceiptModel extends Model
|
||||
{
|
||||
protected $table = 'receipt';
|
||||
protected $primaryKey = 'rcpt_sq';
|
||||
|
||||
public function getReceiptList($params, $pageNum = 1, $pageSize = 20, $total = false)
|
||||
{
|
||||
$builder = $this->db->table('receipt a');
|
||||
|
||||
// 1. SELECT 문 구성
|
||||
$builder->select("
|
||||
a.rcpt_sq, a.comp_sq, a.rcpt_rating, ... (중략) ...,
|
||||
DATE_FORMAT(COALESCE(b.rsrv_date, a.rsrv_date), '%Y-%m-%d') AS rsrv_date,
|
||||
get_code_name('RECEIPT_STATUS1', LEFT(a.rcpt_stat, 2)) AS rcpt_stat_nm,
|
||||
CASE WHEN imgs.has_I1 = 1 THEN 'Y' ELSE 'N' END AS conf_img_yn
|
||||
", false);
|
||||
|
||||
// 2. JOIN 설정
|
||||
$builder->join('result b', 'a.rcpt_sq = b.rcpt_sq', 'left outer');
|
||||
$builder->join('region_codes c', 'a.rcpt_dong = c.region_cd', 'inner');
|
||||
$builder->join('departments d', 'b.dept_sq = d.dept_sq', 'left outer');
|
||||
$builder->join('users u', 'b.usr_sq = u.usr_sq', 'left outer');
|
||||
|
||||
// 서브쿼리 조인 (imgs)
|
||||
$subQuery = $this->db->table('result_imgs')
|
||||
->select("rsrv_sq")
|
||||
->selectMax("CASE WHEN img_type = 'I5' AND use_yn = 'Y' THEN 1 ELSE 0 END", "has_I5")
|
||||
->selectMax("CASE WHEN img_type = 'I1' AND use_yn = 'Y' THEN 1 ELSE 0 END", "has_I1")
|
||||
->groupBy("rsrv_sq")
|
||||
->getCompiledSelect();
|
||||
|
||||
$builder->join("($subQuery) imgs", "imgs.rsrv_sq = b.rsrv_sq", "left", false);
|
||||
|
||||
// 3. WHERE 조건 (권한 및 기본 필터)
|
||||
if (in_array($params['usr_level'], ['4', '40'])) {
|
||||
if (!empty($params['child_dept'])) {
|
||||
$builder->whereIn('b.dept_sq', $params['child_dept']);
|
||||
} else {
|
||||
$builder->where('b.usr_sq', $params['usr_sq']);
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 3개월 데이터 제한
|
||||
$builder->where('a.insert_tm >= DATE_ADD(CURDATE(), INTERVAL -3 MONTH)', null, false);
|
||||
|
||||
// 4. 검색 조건 동적 생성
|
||||
if (!empty($params['rcpt_atclno'])) {
|
||||
$builder->where('a.rcpt_atclno', $params['rcpt_atclno']);
|
||||
} else {
|
||||
if (!empty($params['sdate'])) {
|
||||
$builder->where('a.insert_tm >=', $params['sdate'] . ' 00:00:00');
|
||||
$builder->where('a.insert_tm <', date('Y-m-d', strtotime($params['edate'] . ' +1 day')));
|
||||
}
|
||||
|
||||
// 중개사/매도자 통합 검색 (Group Start/End 사용)
|
||||
if (!empty($params['agent_nm'])) {
|
||||
$builder->groupStart()
|
||||
->like('a.agent_nm', $params['agent_nm'])
|
||||
->orLike('a.sellr_nm', $params['agent_nm'])
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
// 상태(Status) 다중 체크
|
||||
if ($params['stat_all'] !== 'Y' && !empty($params['stat_arr'])) {
|
||||
$builder->groupStart();
|
||||
foreach ($params['stat_arr'] as $stat) {
|
||||
$builder->orLike('a.rcpt_stat', $stat, 'after');
|
||||
}
|
||||
$builder->groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 정렬 및 페이징
|
||||
$builder->orderBy('a.rcpt_atclno', 'desc');
|
||||
|
||||
// 데이터 수 조회를 위해 복제 또는 countAllResults 활용
|
||||
$totalCount = 0;
|
||||
if ($total) {
|
||||
$countBuilder = clone $builder;
|
||||
$totalCount = $countBuilder->countAllResults(false);
|
||||
}
|
||||
|
||||
$offset = ($pageNum - 1) * $pageSize;
|
||||
$result = $builder->get($pageSize, $offset)->getResultArray();
|
||||
|
||||
return [
|
||||
'data' => $result,
|
||||
'total' => $totalCount
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Models/Entities/V2articleinfoModel.php
Normal file
31
app/Models/Entities/V2articleinfoModel.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Entities;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class V2ArticleInfoModel extends Model
|
||||
{
|
||||
protected $table = 'v2_article_info';
|
||||
protected $primaryKey = 'vr_sq';
|
||||
protected $useAutoIncrement = false; // 메인 테이블의 vr_sq를 수동으로 입력받음
|
||||
protected $returnType = 'array';
|
||||
|
||||
protected $allowedFields = [
|
||||
'vr_sq', 'atcl_no', 'cpid', 'cp_atcl_id', 'rlet_type_cd', 'trade_type',
|
||||
'address_code', 'address1', 'address2', 'address3', 'sply_spc', 'excls_spc',
|
||||
'tot_spc', 'grnd_spc', 'bldg_spc', 'deal_amt', 'wrrnt_amt', 'lease_amt',
|
||||
'isale_amt', 'prem_amt', 'sise', 'floor', 'rdate', 'seller_tel_no',
|
||||
'seller_nm', 'realtor_nm', 'realtor_tel_no', 'hscp_no', 'hscp_nm',
|
||||
'ptp_no', 'ptp_nm', 'bild_no', 'charger', 'req_price_yn', 'reg_charger',
|
||||
'dept1_sq', 'dept2_sq', 'reg_dept2_sq', 'reg_dept1_sq', 'floor2',
|
||||
'dong_ho_chk', 'hscplqry_lv', 'ownerNm', 'ownerTelNo', 'chg_trade_type',
|
||||
'chg_address2', 'chg_address3', 'chg_seller_tel', 'chg_amt', 'reg_status',
|
||||
'cupnNo', 'roomSiteAtclRgstCnt', 'roomSiteAtclExpsCnt', 'redvlp_area_nm',
|
||||
'biz_stp_desc', 'cert_register', 'direct_trad_yn', 'confirm_doc_img_url',
|
||||
'confirm_doc_owner_check_yn', 'owner_birth', 'vrfc_type_sub',
|
||||
'cert_register_save_yn', 'confirm_doc_img_url_save_yn', 'address4',
|
||||
'reference_file_url', 'reference_file_url_save_yn', 'reference_file_url_yn',
|
||||
'registerBookUniqueNo', 'relationSellerAndOwner', 'ownerTypeCode', 'registerBookUniqueNumber'
|
||||
];
|
||||
}
|
||||
22
app/Models/Entities/V2articleinfoetcModel.php
Normal file
22
app/Models/Entities/V2articleinfoetcModel.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Entities;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class V2ArticleInfoEtcModel extends Model
|
||||
{
|
||||
protected $table = 'v2_article_info_etc';
|
||||
protected $primaryKey = 'vr_sq';
|
||||
protected $useAutoIncrement = false; // 메인 테이블의 vr_sq를 수동으로 입력받음
|
||||
protected $returnType = 'array';
|
||||
|
||||
protected $allowedFields = [
|
||||
'vr_sq', 'atcl_no', 'corp_own', 'vir_addr_yn', 'bild_no', 'vrfcMthdTpcd',
|
||||
'cert_uncnfrm_status', 'expsStartYmdt', 'vrfcAutoPassYn', 'address2a',
|
||||
'address2b', 'registerBookUniqueNo', 'ownerTypeCode', 'orgRepCphNo',
|
||||
'orgRepTelNo', 'orgRltrNm', 'orgRepNm', 'smsSendTime', 'document_cert_method',
|
||||
'noRgbkVrfcReqYn', 'areaByBdbkVrfcReqYn', 'orgAtclNo', 'atclStatCd',
|
||||
'repNm', 'cpName', 'document_not_received', 'final_failure'
|
||||
];
|
||||
}
|
||||
43
app/Models/Entities/V2chghistoryModel.php
Normal file
43
app/Models/Entities/V2chghistoryModel.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class V2chgstatModel extends Model
|
||||
{
|
||||
protected $table = 'v2_chg_history';
|
||||
protected $primaryKey = 'seq'; // 실제 PK 컬럼명으로 수정하세요 (st_date, cpid, gbn_cd가 복합키인 경우도 있음)
|
||||
protected $allowedFields = ['vr_sq', 'stat_cd', 'chg_type', 'memo', 'insert_id', 'insert_tm'];
|
||||
|
||||
protected $useTimestamps = false; // insert_tm을 직접 넣으시므로 false
|
||||
|
||||
/**
|
||||
* 상태 변경 이력 저장 (CI4 통합 버전)
|
||||
* @param array $data ['vr_sq' => 값, 'stat_cd' => 값, ...]
|
||||
* @param string $saveType 'I'(Upsert), 'U'(Update)
|
||||
*/
|
||||
public function v2_savehistory(array $data )
|
||||
{
|
||||
$payload = [
|
||||
'vr_sq' => $data['vr_sq'],
|
||||
'stat_cd' => $data['stat_cd'],
|
||||
'chg_type' => $data['chg_type'],
|
||||
'memo' => $data['memo'] ?? '',
|
||||
'insert_id' => $data['insert_id'] ?? '0',
|
||||
'insert_tm' => $data['insert_tm'] ?? db_now(),
|
||||
];
|
||||
|
||||
// insert 수행
|
||||
if (!$this->insert($payload)) {
|
||||
return [
|
||||
'error' => [
|
||||
'code' => $this->db->error()['code'],
|
||||
'message' => $this->db->error()['message'],
|
||||
],
|
||||
'query' => (string)$this->getLastQuery()
|
||||
];
|
||||
}
|
||||
|
||||
return ['error' => ['code' => 0, 'message' => ''], 'id' => $this->getInsertID()];
|
||||
}
|
||||
}
|
||||
50
app/Models/Entities/V2chgstatModel.php
Normal file
50
app/Models/Entities/V2chgstatModel.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class V2chgstatModel extends Model
|
||||
{
|
||||
protected $table = 'v2_chg_stat';
|
||||
protected $primaryKey = 'seq'; // 실제 PK 컬럼명으로 수정하세요 (st_date, cpid, gbn_cd가 복합키인 경우도 있음)
|
||||
protected $allowedFields = ['vr_sq', 'stat_cd', 'insert_user', 'insert_tm'];
|
||||
|
||||
protected $useTimestamps = false; // insert_tm을 직접 넣으시므로 false
|
||||
|
||||
/**
|
||||
* 상태 변경 이력 저장 (CI4 통합 버전)
|
||||
* @param array $data ['vr_sq' => 값, 'stat_cd' => 값, ...]
|
||||
* @param string $saveType 'I'(Upsert), 'U'(Update)
|
||||
*/
|
||||
public function saveChgstat(array $data, string $saveType)
|
||||
{
|
||||
// 1. 기본값 세팅 (데이터 유연성 확보)
|
||||
$payload = [
|
||||
'vr_sq' => $data['vr_sq'] ?? null,
|
||||
'stat_cd' => $data['stat_cd'] ?? '10', // 기본값 30
|
||||
'insert_user' => $data['insert_user'] ?? 0,
|
||||
'insert_tm' => $data['insert_tm'] ?? date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
if (empty($payload['vr_sq'])) {
|
||||
throw new \Exception("V2chgstatModel Error: vr_sq is required.");
|
||||
}
|
||||
|
||||
if ($saveType === 'I') {
|
||||
// CI2 방식의 ON DUPLICATE KEY UPDATE 유지 (seq 번호 보존을 위해)
|
||||
$sql = "INSERT INTO v2_chg_stat (vr_sq, stat_cd, insert_user, insert_tm)
|
||||
VALUES (:vr_sq:, :stat_cd:, :insert_user:, :insert_tm:)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
insert_user = VALUES(insert_user),
|
||||
insert_tm = VALUES(insert_tm)";
|
||||
|
||||
return $this->db->query($sql, $payload);
|
||||
} else {
|
||||
// Update 방식
|
||||
return $this->where('vr_sq', $payload['vr_sq'])
|
||||
->where('stat_cd', $payload['stat_cd'])
|
||||
->set($payload)
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
app/Models/Entities/V2stdailyModel.php
Normal file
50
app/Models/Entities/V2stdailyModel.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class V2stdailyModel extends Model
|
||||
{
|
||||
protected $table = 'v2_st_daily';
|
||||
protected $primaryKey = 'id'; // 실제 PK 컬럼명으로 수정하세요 (st_date, cpid, gbn_cd가 복합키인 경우도 있음)
|
||||
protected $allowedFields = ['st_date', 'cpid', 'gbn_cd', 'cnt'];
|
||||
|
||||
public function set_v2_st_daily($st_date, $cpid, $gbn_cd, $cnt, $cnt_type = 'add')
|
||||
{
|
||||
if (empty($cnt)) $cnt = 0;
|
||||
|
||||
$data = [];
|
||||
// 1. 날짜 처리
|
||||
$date_field = empty($st_date) ? "NOW()" : "?";
|
||||
if (!empty($st_date)) $data[] = $st_date;
|
||||
|
||||
// 2. 나머지 필드 바인딩 데이터 준비
|
||||
$data[] = $cpid;
|
||||
$data[] = $gbn_cd;
|
||||
$data[] = $cnt;
|
||||
|
||||
// 3. 중복 처리 로직 분기
|
||||
if (strtolower($cnt_type) === 'add') {
|
||||
// MariaDB에서 가장 안전한 바인딩 방식
|
||||
$sql = "INSERT INTO v2_st_daily (st_date, cpid, gbn_cd, cnt)
|
||||
VALUES ($date_field, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE cnt = cnt + ?";
|
||||
$data[] = $cnt; // UPDATE 절의 더하기 값을 위해 한 번 더 추가
|
||||
} else {
|
||||
// 중복 시 값을 덮어씌움 (VALUES 함수 사용)
|
||||
$sql = "INSERT INTO v2_st_daily (st_date, cpid, gbn_cd, cnt)
|
||||
VALUES ($date_field, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE cnt = VALUES(cnt)";
|
||||
}
|
||||
|
||||
// 쿼리 실행
|
||||
$result = $this->db->query($sql, $data);
|
||||
|
||||
return [
|
||||
'status' => $result ? true : false,
|
||||
'error' => $this->db->error(), // ['code', 'message']
|
||||
'last_query' => (string)$this->db->getLastQuery() // 디버깅용
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
94
app/Models/Entities/VrfcReqModel.php
Normal file
94
app/Models/Entities/VrfcReqModel.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
class VrfcReqModel extends Model {
|
||||
// Model implementation here
|
||||
protected $table = 'v2_vrfc_req';
|
||||
protected $primaryKey = 'vr_sq';
|
||||
|
||||
// 기본값 (명시 안 해도 됨)
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
|
||||
// public function insertV2Article(array $articleInfo): bool
|
||||
// {
|
||||
// // Insert data into the database
|
||||
// // This is a placeholder implementation
|
||||
|
||||
|
||||
|
||||
|
||||
// $articleNumber = $articleInfo['articleNumber']; // 매물 번호와 동일
|
||||
// $cpId = $articleInfo['cpId']; // CPID
|
||||
// $cpArticleNumber = $articleInfo['cpArticleNumber']; // CP 매물번호
|
||||
|
||||
// $rcpt_product = $articleInfo['realEstateTypeCode']; // 매물종류(코드)
|
||||
// $rcpt_product_nm = $articleInfo['realEstateType']; // 매물종류(한글명) // 아파트... ......
|
||||
|
||||
// $rcpt_deal_type = $articleInfo['tradeTypeCode']; // 거래구분(코드) A1, B1, B2
|
||||
// $rcpt_product_info1 = $articleInfo['tradeType']; // 거래구분(한글명) 매매, 전세, 월세
|
||||
|
||||
// $statusTypeCode = $articleInfo['statusTypeCode']; // E12 ...매물 상태 코드
|
||||
|
||||
// $vrfc_type_code = $articleInfo['verificationTypeCode']; // 확인유형코드 S, D, N, M, T, O
|
||||
// $owenrTypeCode = $articleInfo['ownerTypeCode'] ?? ''; // 소유자 유형 코드
|
||||
// $isUnregisteredVerificationRequested = $articleInfo['isUnregisteredVerificationRequested'] ?? false; // 미등기 확인요청 여부
|
||||
// $isBuildingRegisterAreaCheckRequested = $articleInfo['isBuildingRegisterAreaCheckRequested'] ?? false; // 건축물대장 면적확인 요청 여부
|
||||
// $isAutoVerificationRequested = $articleInfo['isAutoVerificationRequested'] ?? false; // 자동확인 요청 여부
|
||||
// // $verificationReference = $articleInfo['verificationReference'] ?? ''; // 확인참고사항
|
||||
// $exposureStartDateTime = $articleInfo['exposureStartDateTime'] ?? ''; // 노출시작일시
|
||||
|
||||
// $facilities['roomCount'] = $articleInfo['facilities']['roomCount'] ?? 0;
|
||||
// $facilities['bathroomCount'] = $articleInfo['facilities']['bathroomCount'] ?? 0;
|
||||
|
||||
// $address['legalDivision']['cityNumber'] = $articleInfo['address']['legalDivision']['cityNumber'] ?? '';
|
||||
// $address['legalDivision']['divisionNumber'] = $articleInfo['address']['legalDivision']['divisionNumber'] ?? '';
|
||||
// $address['legalDivision']['sectorNumber'] = $articleInfo['address']['legalDivision']['sectorNumber'] ?? '';
|
||||
// $address['legalDivision']['legalDivisionAddress'] = $articleInfo['address']['legalDivision']['legalDivisionAddress'] ?? '';
|
||||
// $address['complexNumber'] = $articleInfo['address']['complexNumber'] ?? null;
|
||||
// $address['complexName'] = $articleInfo['address']['complexName'] ?? null;
|
||||
// $address['pyeongTypeNumber'] = $articleInfo['address']['pyeongTypeNumber'] ?? null;
|
||||
// $address['hoName'] = $articleInfo['address']['hoName'] ?? null;
|
||||
|
||||
// $address['isVirtualAddress'] = $articleInfo['address']['isVirtualAddress'] ?? false;
|
||||
// $address['correspondenceFloorCount'] = $articleInfo['address']['correspondenceFloorCount'] ?? 0;
|
||||
// $address['longitude'] = $articleInfo['address']['longitude'] ?? 0;
|
||||
// $address['latitude'] = $articleInfo['address']['latitude'] ?? 0;
|
||||
// $address['isDongHoChecked'] = $articleInfo['address']['isDongHoChecked'] ?? null;
|
||||
// $address['inquiryLevel'] = $articleInfo['address']['inquiryLevel'] ?? null;
|
||||
|
||||
// $space['totalSpace'] = $articleInfo['space']['totalSpace'] ?? null;
|
||||
// $space['groundSpace'] = $articleInfo['space']['groundSpace'] ?? null;
|
||||
// $space['buildingSpace'] = $articleInfo['space']['buildingSpace'] ?? null;
|
||||
// $space['supplySpace'] = $articleInfo['space']['supplySpace'] ?? 0;
|
||||
// $space['exclusiveSpace'] = $articleInfo['space']['exclusiveSpace'] ?? 0;
|
||||
|
||||
// $price['dealAmount'] = $articleInfo['price']['dealAmount'] ?? 0;
|
||||
// $price['warrantyAmount'] = $articleInfo['price']['warrantyAmount'] ?? 0;
|
||||
// $price['leaseAmount'] = $articleInfo['price']['leaseAmount'] ?? 0;
|
||||
|
||||
// $floor['correspondenceFloorCount'] = $articleInfo['floor']['correspondenceFloorCount'] ?? 0;
|
||||
// $floor['correspondenceFloorType'] = $articleInfo['floor']['correspondenceFloorType'] ?? null;
|
||||
// $floor['totalFloorCount'] = $articleInfo['floor']['totalFloorCount'] ?? 0;
|
||||
// $floor['undergroundFloorCount'] = $articleInfo['floor']['undergroundFloorCount'] ?? 0;
|
||||
|
||||
// $seller['sellerTelephoneNumber'] = $articleInfo['seller']['sellerTelephoneNumber'] ?? null;
|
||||
// $seller['sellerName'] = $articleInfo['seller']['sellerName'] ?? null;
|
||||
// $seller['ownerTelephoneNumber'] = $articleInfo['seller']['ownerTelephoneNumber'] ?? null;
|
||||
// $seller['ownerName'] = $articleInfo['seller']['ownerName'] ?? null;
|
||||
// $seller['isOwnerCertificationAgree'] = $articleInfo['seller']['isOwnerCertificationAgree'] ?? null;
|
||||
// $seller['isDirectTrade'] = $articleInfo['seller']['isDirectTrade'] ?? null;
|
||||
|
||||
// $realtor['realtorName'] = $articleInfo['realtor']['realtorName'] ?? null;
|
||||
// $realtor['representativeCellphoneNumber'] = $articleInfo['realtor']['representativeCellphoneNumber'] ?? null;
|
||||
// $realtor['representativeTelephoneNumber'] = $articleInfo['realtor']['representativeTelephoneNumber'] ?? null;
|
||||
|
||||
// $files = $articleInfo['files'] ?? [];
|
||||
|
||||
|
||||
|
||||
|
||||
// return true;
|
||||
// }
|
||||
}
|
||||
@@ -788,4 +788,310 @@ class M705Model extends Model
|
||||
|
||||
return $query->getResultArray();
|
||||
}
|
||||
|
||||
|
||||
// 상세정보
|
||||
public function getDetail($vr_sq)
|
||||
{
|
||||
$sql = "SELECT
|
||||
a.vr_sq,
|
||||
a.dong_ho_chk,
|
||||
a.reg_status,
|
||||
a.hscplqry_lv,
|
||||
a.atcl_no,
|
||||
b.stat_cd,
|
||||
a.cpid,
|
||||
a.cp_atcl_id,
|
||||
a.rlet_type_cd,
|
||||
a.address1,
|
||||
a.sise,
|
||||
a.rdate,
|
||||
a.hscp_no as chk_hscp_no,
|
||||
b.try_cnt,
|
||||
a.seller_tel_no,
|
||||
a.seller_nm,
|
||||
a.realtor_nm,
|
||||
a.realtor_tel_no,
|
||||
a.charger,
|
||||
a.ownerNm,
|
||||
a.ownerTelNo,
|
||||
b.reg_try_cnt,
|
||||
b.insert_tm,
|
||||
a.reg_charger,
|
||||
i2.usr_nm as reg_charger_nm,
|
||||
c.bild_nm,
|
||||
b.vrfc_type as vrfc_type_cd,
|
||||
c.rm_no,
|
||||
c.floor,
|
||||
c.floor2,
|
||||
c.address_code,
|
||||
c.address2,
|
||||
c1.address2a,
|
||||
c1.address2b,
|
||||
c.address3,
|
||||
c.address4,
|
||||
c.trade_type as trade_type_cd,
|
||||
c.deal_amt,
|
||||
c.wrrnt_amt,
|
||||
c.lease_amt,
|
||||
c.isale_amt,
|
||||
c.prem_amt,
|
||||
c.sply_spc,
|
||||
c.excls_spc,
|
||||
c.tot_spc,
|
||||
c.grnd_spc,
|
||||
c.bldg_spc,
|
||||
c.hscp_no,
|
||||
c.ptp_no,
|
||||
d.insert_tm as update_res_tm,
|
||||
e.insert_tm as result_tm,
|
||||
f.region_nm,
|
||||
g.cd_nm as pre_stat,
|
||||
g.cd as pre_stat_cd,
|
||||
h.cd_nm as vrfc_type,
|
||||
i.usr_nm,
|
||||
j.cd_nm as trade_type,
|
||||
j.cd as trade_type_cd,
|
||||
c.hscp_nm,
|
||||
c.ptp_nm,
|
||||
l.success,
|
||||
k.cd_nm as atcl_nm,
|
||||
m.code as result_d11,
|
||||
m.comment,
|
||||
n.code as fax_conf_yn_2,
|
||||
o.code as fax_conf_yn_3,
|
||||
p.code as fax_conf_yn_4,
|
||||
n.comment as fax_conf_yn_info_2,
|
||||
o.comment as fax_conf_yn_info_3,
|
||||
p.comment as fax_conf_yn_info_4,
|
||||
v.success AS tel_suc,
|
||||
r.code AS tel_agree,
|
||||
s.code AS tel_conf_yn_2,
|
||||
t.code AS tel_conf_yn_3,
|
||||
u.code AS tel_conf_yn_4,
|
||||
s.comment AS tel_conf_yn_info_2,
|
||||
t.comment AS tel_conf_yn_info_3,
|
||||
u.comment AS tel_conf_yn_info_4,
|
||||
w.success AS reg_conf_yn_1,
|
||||
x.code AS reg_conf_yn_2,
|
||||
y.code AS reg_conf_yn_3,
|
||||
x.comment AS reg_conf_yn_info_2,
|
||||
y.comment AS reg_conf_yn_info_3,
|
||||
b.rgbk_confirm,
|
||||
a.redvlp_area_nm,
|
||||
a.biz_stp_desc,
|
||||
a.cert_register,
|
||||
a.confirm_doc_img_url,
|
||||
a.cert_register_save_yn,
|
||||
a.confirm_doc_img_url_save_yn,
|
||||
b.confirm_doc_owner_check_yn,
|
||||
a.owner_birth,
|
||||
a.vrfc_type_sub,
|
||||
b.owner_verifiable,
|
||||
a.reference_file_url,
|
||||
a.reference_file_url_save_yn,
|
||||
a.reference_file_url_yn,
|
||||
z.corp_own,
|
||||
c1.vir_addr_yn,
|
||||
c1.cert_uncnfrm_status,
|
||||
c1.noRgbkVrfcReqYn,
|
||||
c1.areaByBdbkVrfcReqYn,
|
||||
sm.sm_apporval_date ,
|
||||
sm.sm_end_date,
|
||||
sm.sm_seq,
|
||||
a.registerBookUniqueNumber,
|
||||
(select count(*) from v2_article_fail d3 where d3.vr_sq = a.vr_sq ) as final_fail_cnt
|
||||
FROM v2_article_info a
|
||||
JOIN v2_vrfc_req b ON a.vr_sq = b.vr_sq
|
||||
JOIN v2_modify_info c ON a.vr_sq = c.vr_sq
|
||||
LEFT JOIN v2_article_info_etc c1 ON c1.vr_sq = a.vr_sq
|
||||
LEFT JOIN region_codes f ON a.address_code = f.region_cd
|
||||
LEFT JOIN v2_chg_stat d ON a.vr_sq = d.vr_sq AND d.stat_cd = '35'
|
||||
LEFT JOIN v2_chg_stat e ON a.vr_sq = e.vr_sq AND e.stat_cd = '60'
|
||||
LEFT JOIN codes g ON b.stat_cd = g.cd AND g.category = 'STEP_VERIFICATION'
|
||||
LEFT JOIN codes h ON b.vrfc_type = h.cd AND h.category = 'VRFCREQ_WAY'
|
||||
LEFT JOIN codes j ON c.trade_type = j.cd AND j.category = 'TRADE_TYPE'
|
||||
LEFT JOIN codes k ON a.rlet_type_cd = k.cd AND k.category = 'ARTICLE_TYPE'
|
||||
LEFT JOIN v2_confirm l ON a.vr_sq = l.vr_sq AND l.vrfc_type = 'D'
|
||||
LEFT JOIN v2_check_list m ON a.vr_sq = m.vr_sq AND m.type = 'D11'
|
||||
LEFT JOIN v2_check_list n ON a.vr_sq = n.vr_sq AND n.type = 'D12'
|
||||
LEFT JOIN v2_check_list o ON a.vr_sq = o.vr_sq AND o.type = 'D13'
|
||||
LEFT JOIN v2_check_list p ON a.vr_sq = p.vr_sq AND p.type = 'D14'
|
||||
LEFT JOIN v2_confirm v ON a.vr_sq = v.vr_sq AND v.vrfc_type = 'T'
|
||||
LEFT JOIN v2_check_list r ON a.vr_sq = r.vr_sq AND r.type = 'T11'
|
||||
LEFT JOIN v2_check_list s ON a.vr_sq = s.vr_sq AND s.type = 'T12'
|
||||
LEFT JOIN v2_check_list t ON a.vr_sq = t.vr_sq AND t.type = 'T13'
|
||||
LEFT JOIN v2_check_list u ON a.vr_sq = u.vr_sq AND u.type = 'T14'
|
||||
LEFT JOIN v2_confirm w ON a.vr_sq = w.vr_sq AND w.vrfc_type = 'R'
|
||||
LEFT JOIN v2_check_list x ON a.vr_sq = x.vr_sq AND x.type = '21'
|
||||
LEFT JOIN v2_check_list y ON a.vr_sq = y.vr_sq AND y.type = '22'
|
||||
LEFT JOIN users i ON a.charger = i.usr_id
|
||||
LEFT JOIN users i2 ON a.reg_charger = i2.usr_id
|
||||
LEFT JOIN v2_article_info_etc z ON a.vr_sq = z.vr_sq
|
||||
LEFT JOIN scomplex_manage sm ON a.hscp_no = sm.sm_code
|
||||
|
||||
|
||||
WHERE a.vr_sq = " . $vr_sq;
|
||||
|
||||
$query = $this->db->query($sql);
|
||||
|
||||
return $query->getRowArray();
|
||||
}
|
||||
|
||||
public function getRecordInfo($vr_sq, $file_type)
|
||||
{
|
||||
$sql = "SELECT seq, vr_sq, use_yn, file_type, view_odr, file_path, file_name, file_ext, file_size, img_width, img_height, meta_data, insert_user, insert_tm , cloud_upload_yn " .
|
||||
" FROM v2_files" .
|
||||
" WHERE vr_sq = ?" .
|
||||
" AND use_yn = 'Y'" .
|
||||
" AND file_type = ?" .
|
||||
" ORDER BY seq DESC";
|
||||
$data = [
|
||||
$vr_sq,
|
||||
$file_type
|
||||
];
|
||||
|
||||
$query = $this->db->query($sql, [$vr_sq, $file_type]);
|
||||
|
||||
return $query->getRowArray();
|
||||
}
|
||||
|
||||
// 메모
|
||||
public function getMemo($vr_sq)
|
||||
{
|
||||
$sql = "SELECT memo FROM v2_vrfc_req where vr_sq = ?";
|
||||
|
||||
$query = $this->db->query($sql, [$vr_sq]);
|
||||
|
||||
return $query->getRowArray();
|
||||
}
|
||||
|
||||
public function getDisplay($menu_position)
|
||||
{
|
||||
$sql = "select display_yn " .
|
||||
"from page_display " .
|
||||
"where menu_position = ? ";
|
||||
$data = [$menu_position];
|
||||
$query = $this->db->query($sql, $data);
|
||||
|
||||
return $query->getRowArray();
|
||||
}
|
||||
|
||||
/* 모든 이미지 파일 */
|
||||
public function getAllRecordInfo($vr_sq, $file_type)
|
||||
{
|
||||
$sql = "SELECT seq, vr_sq, use_yn, file_type, view_odr, file_path, file_name, file_ext, file_size, img_width, img_height, meta_data, insert_user, insert_tm " .
|
||||
" FROM v2_files" .
|
||||
" WHERE vr_sq = ?" .
|
||||
" AND file_type = ?";
|
||||
$data = [
|
||||
$vr_sq,
|
||||
$file_type
|
||||
];
|
||||
|
||||
$query = $this->db->query($sql, $data);
|
||||
|
||||
return $query->getResultArray();
|
||||
}
|
||||
|
||||
// 법인저장
|
||||
public function saveCorp($vr_sq, $atcl_no)
|
||||
{
|
||||
$sql = "INSERT v2_article_info_etc(vr_sq,atcl_no,corp_own)" .
|
||||
" VALUES(?,?,'Y')" .
|
||||
" ON DUPLICATE KEY UPDATE corp_own='Y'";
|
||||
$data = [
|
||||
$vr_sq,
|
||||
$atcl_no
|
||||
];
|
||||
|
||||
if ($this->db->query($sql, $data) === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => '저장 실패',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true
|
||||
];
|
||||
}
|
||||
|
||||
// 파일업로드
|
||||
public function saveFileInfo($data)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
// 기존파일 확인후 업데이트
|
||||
$sql = "SELECT seq FROM v2_files WHERE vr_sq = {$data['vr_sq']} AND use_yn = 'Y' AND file_type = '2'";
|
||||
$query = $this->db->query($sql);
|
||||
$row = $query->getNumRows();
|
||||
|
||||
if ($row > 0) {
|
||||
|
||||
$sql = "UPDATE v2_files SET use_yn = 'N' WHERE vr_sq = {$data['vr_sq']} AND use_yn = 'Y' AND file_type '2'";
|
||||
|
||||
$this->db->query($sql);
|
||||
|
||||
$sql = "INSERT INTO v2_files
|
||||
(vr_sq, file_type, view_odr, file_path, file_name, file_ext, file_size, insert_user, insert_tm, cloud_upload_yn)
|
||||
VALUES
|
||||
(?, '2', 0, ?, ?, ?, ?, ?, NOW(), 'Y')
|
||||
";
|
||||
|
||||
$param = [
|
||||
$data['vr_sq'],
|
||||
$data['file_path'],
|
||||
$data['new_name'],
|
||||
$data['ext'],
|
||||
$data['size'],
|
||||
$data['usr_id'],
|
||||
];
|
||||
|
||||
|
||||
if ($this->db->query($sql, $param)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => '파일정보 저장 실패',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return [
|
||||
'success' => true
|
||||
];
|
||||
}
|
||||
|
||||
// 다음매물확인
|
||||
public function getNextInfo($vr_sq)
|
||||
{
|
||||
$this->db->transStart();
|
||||
$usr_id = session('usr_id');
|
||||
|
||||
$sql = " SELECT b.vr_sq" .
|
||||
" FROM v2_article_info b" .
|
||||
" INNER JOIN v2_vrfc_req a ON a.vr_sq = b.vr_sq AND a.vr_sq != ? AND a.rgbk_confirm = '1' AND a.stat_cd BETWEEN '35' AND '49' AND a.stat_cd NOT IN ('35','39','45')" .
|
||||
" LEFT JOIN v2_chg_stat c ON c.vr_sq = b.vr_sq AND c.stat_cd = '35'" .
|
||||
" WHERE a.insert_tm < DATE_FORMAT(curdate(), '%Y%m%d172959')" .
|
||||
" AND (b.reg_charger IS NULL OR b.reg_charger = '')" .
|
||||
" AND a.vrfc_type NOT IN ( 'N' , 'O' ) " .
|
||||
" ORDER BY CASE a.vrfc_type WHEN 'M' THEN 1 ELSE 2 END, a.vr_sq" .
|
||||
" LIMIT 1" .
|
||||
" FOR UPDATE skip locked";
|
||||
|
||||
$query = $this->db->query($sql, [$vr_sq]);
|
||||
$row = $query->getRowArray();
|
||||
|
||||
$sql = "UPDATE v2_article_info" .
|
||||
" SET reg_charger='" . $usr_id . "'" .
|
||||
" WHERE vr_sq = '" . $row['vr_sq'] . "'";
|
||||
$this->db->query($sql);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
246
app/Services/NaverService.php
Normal file
246
app/Services/NaverService.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
class NaverService
|
||||
{
|
||||
protected $naverClient, $VrfcReqModel, $V2stdailyModel, $V2chgstatModel, $V2chghistoryModel;
|
||||
|
||||
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);
|
||||
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");
|
||||
}
|
||||
|
||||
$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);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception("알 수 없는 requestType: $requestType");
|
||||
}
|
||||
|
||||
return ['vr_sq' => $vr_sq, 'articleNumber' => $articleNumber];
|
||||
}
|
||||
|
||||
/**
|
||||
* [REG] 신규 등록
|
||||
*/
|
||||
private function insertVrfcReq($articleNumber, $params)
|
||||
{
|
||||
$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->recordStatusAndHistory($vr_sq, '10', 'C9', "신규접수 : 10");
|
||||
|
||||
return $vr_sq;
|
||||
}
|
||||
|
||||
/**
|
||||
* [MOD] 수정 처리
|
||||
*/
|
||||
private function updateVrfcReq($articleNumber, $params)
|
||||
{
|
||||
$existing = $this->findExisting($articleNumber);
|
||||
if (!$existing) return $this->insertVrfcReq($articleNumber, $params);
|
||||
|
||||
$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)
|
||||
{
|
||||
$existing = $this->findExisting($articleNumber);
|
||||
$params['stat_cd'] = '19';
|
||||
$params['req_type'] = 'D';
|
||||
|
||||
return $this->updateProcess($existing, $params, 'CNC', "취소 처리: {$existing['stat_cd']} => 19");
|
||||
}
|
||||
|
||||
/**
|
||||
* [FIN] 완료 처리
|
||||
*/
|
||||
private function finVrfcReq($articleNumber, $params)
|
||||
{
|
||||
$existing = $this->findExisting($articleNumber);
|
||||
$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->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 컬럼에 맞게 변환
|
||||
*/
|
||||
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'];
|
||||
}
|
||||
}
|
||||
|
||||
$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),
|
||||
];
|
||||
|
||||
return $vrfc_params;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="main-card mb-3 card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center w-100">
|
||||
<span class="me-2">홍보확인서 상세</span>
|
||||
<div class="ms-auto d-flex gap-1">
|
||||
<?php if (($data['receiver'] ?? '') != "API"): ?>
|
||||
<?php if (($data['receiver'] ?? '') != "API"): ?>
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center w-100">
|
||||
<div class="ms-auto d-flex gap-1">
|
||||
|
||||
<span class="text-muted small me-2">
|
||||
발신번호 : <?= esc(str_replace('-', '', $data['caller_no'] ?? '')) ?>
|
||||
</span>
|
||||
@@ -35,11 +35,10 @@
|
||||
onclick="faximage_rotate(180)">180˚</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="faximage_rotate(270)">270˚</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
<div class="card-body">
|
||||
<!-- table 유지 + 반응형 -->
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -452,7 +452,7 @@
|
||||
});
|
||||
|
||||
|
||||
|
||||
initReceiptDate();
|
||||
table = $('#resultList').DataTable({
|
||||
language: lang_kor,
|
||||
serverSide: true,
|
||||
@@ -469,7 +469,7 @@
|
||||
blockUI.unblockPage()
|
||||
},
|
||||
data: function (d) {
|
||||
initReceiptDate();
|
||||
|
||||
|
||||
d.atcl_no = $("#frm_srch_info [name=atcl_no]").val(); // 매물번호
|
||||
d.chk_atcl_no = $("#frm_srch_info [name=chk_atcl_no]").val(); // 매물번호입력
|
||||
|
||||
@@ -1039,7 +1039,7 @@ if (!empty($regist2)) {
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/m703/m703a/getNextFaxImgs',
|
||||
url: '/m704/m704a/getNextTelInfo',
|
||||
contentType: 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
|
||||
1717
app/Views/pages/v2/m705/detail.php
Normal file
1717
app/Views/pages/v2/m705/detail.php
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/plugin/img/pdf.png
Normal file
BIN
public/plugin/img/pdf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
117
worker/api_receiver.php
Normal file
117
worker/api_receiver.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* [A 작업] 네이버 검증 요청 실시간 수신 리시버
|
||||
* - 프레임워크를 로드하지 않아 매우 빠르고 안전함
|
||||
* - 받은 데이터를 Redis 큐에 넣고 즉시 응답
|
||||
*/
|
||||
// 1. 응답 헤더 설정 (JSON)
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// 2. 보안 키 체크 (URL 파라미터 key=값)
|
||||
$configKey = "7EE868F4B36D36B3D86736828F4729EAC4992083"; // 실제 사용할 키값으로 변경하세요
|
||||
$receivedKey = $_GET['key'] ?? '';
|
||||
$logDir = __DIR__ . '/logs/';
|
||||
|
||||
if ($receivedKey !== $configKey) {
|
||||
http_response_code(403);
|
||||
echo apiResponse([
|
||||
'code' => '-1',
|
||||
'message' => 'Unregistered key'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 3. 데이터 수신 (POST JSON 또는 GET 파라미터)
|
||||
$rawData = file_get_contents('php://input');
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
if (empty($data)) {
|
||||
throw new Exception("Empty data received");
|
||||
}
|
||||
|
||||
// 4. Redis 연결
|
||||
$redis = new Redis();
|
||||
// Docker 서비스 이름인 'redis' 사용
|
||||
$success = $redis->connect('redis', 6379);
|
||||
|
||||
if (!$success) {
|
||||
throw new Exception("Could not connect to Redis");
|
||||
}
|
||||
|
||||
$redis->select(9); // 10번 DB 사용
|
||||
|
||||
// 5. 큐에 넣을 데이터 포맷팅
|
||||
$payload = [
|
||||
'request_data' => $data,
|
||||
'received_at' => date('Y-m-d H:i:s'),
|
||||
'client_ip' => $_SERVER['REMOTE_ADDR']
|
||||
];
|
||||
|
||||
// 'naver:raw_queue'라는 이름의 리스트에 저장
|
||||
$redis->lPush('naver:raw_queue', json_encode($payload));
|
||||
|
||||
// --- [여기서부터 로그 저장 코드 추가] ---
|
||||
// 들어온 원본($rawData)을 그대로 기록합니다.
|
||||
writeLog("RAW_RECEIVE | " . $rawData, 'INFO');
|
||||
// --------------------------------------
|
||||
// 6. 네이버측에 성공 응답 (202 Accepted)
|
||||
// 처리가 완료된 것은 아니지만, 접수는 완료되었음을 의미
|
||||
http_response_code(200);
|
||||
echo apiResponse([
|
||||
'code' => 'success',
|
||||
'message' => ''
|
||||
]);
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
// 7. 장애 발생 시 로그 기록 (시스템 로그)
|
||||
writeLog( 'Exception :' . apiResponse($data) , 'ERROR');
|
||||
|
||||
http_response_code(500);
|
||||
|
||||
echo apiResponse([
|
||||
'code' => '-1',
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜별 로그 기록 함수
|
||||
* @param string $message 로그 내용
|
||||
* @param string $level 로그 레벨 (INFO, ERROR, DEBUG 등)
|
||||
*/
|
||||
function writeLog($message, $level = 'ERROR') {
|
||||
// 1. 로그 저장 경로 설정 (프로젝트 루트의 logs 폴더)
|
||||
$logBaseDir = __DIR__ . '/logs';
|
||||
$Dir = $logBaseDir . '/';
|
||||
|
||||
// 2. 폴더가 없으면 생성 (연/월 구조로 관리하면 파일이 너무 많아지는 것을 방지)
|
||||
if (!is_dir($Dir)) {
|
||||
@mkdir($Dir, 0777, true);
|
||||
}
|
||||
|
||||
// 3. 파일명 결정 (예: logs/2025/12/2025-12-22.log)
|
||||
$logFile = $Dir . date('Y-m-d') . '.log';
|
||||
// 4. 로그 포맷팅 (시간 [레벨] 메시지)
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
|
||||
$singleLineMessage = str_replace(["\r", "\n", "\t"], " ", $message);
|
||||
|
||||
$formattedMessage = "[$timestamp] [$level] $singleLineMessage" . PHP_EOL;
|
||||
|
||||
// 5. 파일 기록 (FILE_APPEND로 기존 내용 뒤에 추가)
|
||||
file_put_contents($logFile, $formattedMessage, FILE_APPEND);
|
||||
}
|
||||
|
||||
// 도우미 함수 정의
|
||||
function apiResponse($error = null) {
|
||||
// $base = [
|
||||
// '@type' => 'response',
|
||||
// '@service' => 'confirms',
|
||||
// '@version' => '1.0.0'
|
||||
// ];
|
||||
// if ($error) $base['error'] = $error;
|
||||
$base = $error;
|
||||
return json_encode($base);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Directory access is forbidden.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user