Compare commits

..

37 Commits

Author SHA1 Message Date
yangsh
36b06a66d2 등기부등본 상세추가
Some checks failed
Close Pull Request / main (pull_request_target) Has been cancelled
2026-01-08 11:54:26 +09:00
438f0a546c 등기부등본 목록 추가
Reviewed-on: http://192.168.10.243:3000/owrainfo/confirms/pulls/10
2026-01-07 14:35:01 +09:00
1e5e44e10b 초기화수정
Reviewed-on: http://192.168.10.243:3000/owrainfo/confirms/pulls/9
2026-01-07 13:12:42 +09:00
c129a277b0 Merge pull request '전화확인매물 추가' (#8) from feature/template into master
Reviewed-on: http://192.168.10.243:3000/owrainfo/confirms/pulls/8
2026-01-07 13:06:44 +09:00
c49a7e0a32 Merge pull request 'feature/template' (#7) from feature/template into master
Reviewed-on: http://192.168.10.243:3000/owrainfo/confirms/pulls/7
2026-01-06 17:53:31 +09:00
b971005525 v2chghistoryModel.php 수정 2026-01-05 15:19:27 +09:00
da33e34d4f api 2026-01-05 15:11:12 +09:00
8338df57c9 httpd 코드 수정 202-> 200 2026-01-02 14:50:27 +09:00
094fa7c640 api_receiver 리턴 메세지 수정 2026-01-02 10:05:55 +09:00
yangsh
7627951c09 merge 후 helper 정리 2026-01-02 08:54:41 +09:00
yangsh
cfd2ee2787 Merge branch 'feature/template' 2026-01-02 08:49:43 +09:00
546d23f077 Merge pull request 'feature/template' (#6) from feature/template into master
Reviewed-on: http://192.168.10.243:3000/owrainfo/confirms/pulls/6
2025-12-31 11:09:11 +09:00
249efb0a29 Merge pull request 'feature/template' (#5) from feature/template into master
Reviewed-on: http://192.168.10.243:3000/owrainfo/confirms/pulls/5
2025-12-30 11:13:07 +09:00
dbeb0d6b1f 워커 서비스 생성 및 수정 2025-12-29 20:35:55 +09:00
4cfd6f1faf naverWorker 오류 수정 2025-12-29 17:47:17 +09:00
e085eccaab naverWorker 오류 수정 2025-12-29 17:43:43 +09:00
b42755bc27 naverWorker 오류 수정 2025-12-29 17:07:03 +09:00
22fa8b32a1 naverWorker 오류 수정 2025-12-29 16:03:13 +09:00
318f2c063e ROUTES 대소문자 수정 2025-12-29 15:50:18 +09:00
e377dee571 ROUTES 대소문자 수정 2025-12-29 15:48:58 +09:00
bbe47ad097 ROUTES 대소문자 수정 2025-12-29 15:21:35 +09:00
ac75b7ebf5 naverWorker.php 수정 2025-12-29 15:15:17 +09:00
b03f783051 naverWorker.php 수정 2025-12-29 15:10:00 +09:00
24b0548002 api_info 오류 수정 2025-12-26 15:25:13 +09:00
946bc15aa6 Merge pull request 'merge' (#4) from feature/template into master
Reviewed-on: http://192.168.10.243:3000/owrainfo/confirms/pulls/4
2025-12-23 08:45:26 +00:00
76e79ea4cf php_worker 수정 2025-12-23 16:19:22 +09:00
454bb77a07 php_worker 수정 2025-12-23 15:59:40 +09:00
8864a46c8a php_worker 수정 2025-12-23 15:54:20 +09:00
1d693df861 gitignore 수정 2025-12-23 13:58:07 +09:00
6576b59d7e Remove logs from tracking 2025-12-23 13:57:51 +09:00
c30b30bb07 worker test 2025-12-23 13:53:09 +09:00
0e9915bd7a worker 프로젝트에 추가 2025-12-22 19:38:41 +09:00
11840905f1 rename home to Home 2025-12-22 15:06:25 +09:00
98c36e13f6 temp rename home directory 2025-12-22 15:06:07 +09:00
e4c7b633ce 오류 수정중 2025-12-22 14:41:38 +09:00
a8b66af2fa redis 비밀번호 수정 2025-12-22 14:20:31 +09:00
dbc9d875c3 Merge branch 'feature/template' 2025-12-22 13:58:42 +09:00
27 changed files with 3270 additions and 120 deletions

1
.gitignore vendored
View File

@@ -171,3 +171,4 @@ _modules/*
/dist/
/node_modules/
.env
**/logs/

View File

@@ -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();
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}");
while (true) {
$result = $redis->brPop(['naver:raw_queue'], 30);
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.");
}
}
}

View File

@@ -88,5 +88,5 @@ class Autoload extends AutoloadConfig
*
* @var list<string>
*/
public $helpers = ['url'];
public $helpers = ['log','url', 'function'];
}

View File

@@ -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)

View File

@@ -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');

View File

@@ -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(),
]);
}
}
}

View File

@@ -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 종류 이상

View 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);
}
}

View File

@@ -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;
}
}

View 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);
}
}

View 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';
}

View 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
];
}
}

View 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'
];
}

View 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'
];
}

View 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()];
}
}

View 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();
}
}
}

View 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() // 디버깅용
];
}
}

View 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;
// }
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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">

View File

@@ -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(); // 매물번호입력

View File

@@ -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,

File diff suppressed because it is too large Load Diff

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
View 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);
}

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>