worker service 매물 등록 및 redis 다운시 처리

This commit is contained in:
2026-03-25 20:51:38 +09:00
parent a170fdc774
commit 0b6ed3df73
12 changed files with 976 additions and 199 deletions

View File

@@ -23,136 +23,155 @@ class NaverWorker extends BaseCommand
public function run(array $params)
{
helper('log'); // 여기서 로드 완료!
helper(['log', 'redis']); // redis helper 추가
$this->db = \Config\Database::connect();
$logModel = model(NaverWorkerLogModel::class);
$naverService = new \App\Services\NaverService(); // 서비스 생성
$redis = new \Redis();
try {
// 두 가지 환경 변수 형식 지원 (REDIS_HOST 또는 redis.default.host)
$this->redisHost = getenv('REDIS_HOST') ?: (getenv('redis.default.host') ?: '127.0.0.1');
$this->redisPort = getenv('REDIS_PORT') ?: (getenv('redis.default.port') ?: 6379);
$this->redisDatabase = getenv('REDIS_DATABASE') ?: (getenv('redis.default.database') ?: 9);
$redis->connect($this->redisHost, (int)$this->redisPort);
$redis->select((int)$this->redisDatabase);
CLI::write(CLI::color('🟢 Naver Worker running... (Redis: ' . $this->redisHost . ':' . $this->redisPort . ' DB:' . $this->redisDatabase . ')', 'green'));
} catch (\Exception $e) {
CLI::error("Redis 연결 불가: " . $e->getMessage());
return;
// Redis 연결 (실패해도 계속 진행 - 파일 모드로 동작 가능)
$redis = get_redis_connection('worker');
$config = get_redis_config('worker');
if ($redis) {
CLI::write(CLI::color('🟢 Naver Worker running... (Redis: ' . $config['host'] . ':' . $config['port'] . ' DB:' . $config['database'] . ')', 'green'));
} else {
CLI::write(CLI::color('⚠️ Naver Worker running in FILE-ONLY mode (Redis unavailable)', 'yellow'));
}
while (true) {
// Redis brPop with retry logic
$maxRetries = 3;
$retryCount = 0;
$result = null;
while ($retryCount < $maxRetries) {
try {
$result = $redis->brPop(['naver:raw_queue'], 30);
break; // 성공하면 루프 탈출
} catch (\RedisException $e) {
$retryCount++;
CLI::write(CLI::color("⚠️ Redis error (attempt {$retryCount}/{$maxRetries}): " . $e->getMessage(), 'yellow'));
if ($retryCount >= $maxRetries) {
CLI::error("❌ Redis reconnection failed after {$maxRetries} attempts");
break;
}
// Redis 재연결 시도
// Redis 또는 폴백 파일에서 데이터 읽기
$rawData = null;
$source = 'redis'; // 데이터 소스 추적
// 1. Redis에서 데이터 읽기 시도 (Redis가 있을 경우만)
if ($redis) {
$maxRetries = 2;
$retryCount = 0;
while ($retryCount < $maxRetries) {
try {
CLI::write(CLI::color('🔄 Reconnecting to Redis...', 'yellow'));
$redis->close();
$redis->connect($this->redisHost, (int)$this->redisPort);
$redis->select((int)$this->redisDatabase);
CLI::write(CLI::color('✅ Redis reconnected', 'green'));
} catch (\Exception $reconnectError) {
CLI::error("Redis reconnection error: " . $reconnectError->getMessage());
sleep(2); // 재연결 실패 시 잠시 대기
$result = $redis->brPop(['naver:raw_queue'], 5); // 5초 타임아웃
if ($result) {
$rawData = $result[1];
$source = 'redis';
}
break; // 성공하면 루프 탈출
} catch (\Exception $e) {
$retryCount++;
CLI::write(CLI::color("⚠️ Redis error (attempt {$retryCount}/{$maxRetries}): " . $e->getMessage(), 'yellow'));
if ($retryCount >= $maxRetries) {
CLI::write(CLI::color("⚠️ Redis unavailable, switching to file mode", 'yellow'));
$redis = null; // Redis를 비활성화
break;
}
// Redis 재연결 시도
try {
CLI::write(CLI::color('🔄 Reconnecting to Redis...', 'yellow'));
$redis->close();
$redis = get_redis_connection('worker');
if ($redis) {
CLI::write(CLI::color('✅ Redis reconnected', 'green'));
}
} catch (\Exception $reconnectError) {
CLI::error("Redis reconnection error: " . $reconnectError->getMessage());
}
}
}
}
if (!$result) {
// 데이터가 없어서 타임아웃 난 경우.
// 굳이 sleep 안 해도 바로 다음 brPop이 다시 30초 대기를 시작함.
// 2. Redis에서 데이터 없으면 폴백 파일 확인
if (!$rawData) {
$rawData = $this->readFromFallbackFile();
if ($rawData) {
$source = 'file';
}
}
// 3. 데이터 없으면 다음 루프
if (!$rawData) {
continue;
}
if ($result) {
$rawData = $result[1];
// 4. 데이터 소스 로깅
CLI::write(CLI::color("📥 Data received from: " . strtoupper($source), 'cyan'));
// [1] 꺼내자마자 DB에 원문 저장 (2차 임시 저장) - 실패 시 재시도
try {
$logId = $logModel->insert([
'raw_payload' => $rawData,
'status' => 'INIT'
]);
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
// MySQL gone away 에러 시 재연결 후 재시도
if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) {
CLI::write(CLI::color('⚠️ MySQL gone away, reconnecting...', 'yellow'));
$this->db->close();
$this->db = \Config\Database::connect();
$logModel = model(NaverWorkerLogModel::class);
// 재시도
$logId = $logModel->insert([
'raw_payload' => $rawData,
'status' => 'INIT'
]);
} else {
throw $e; // 다른 에러면 그대로 throw
}
}
try {
$responseJson = json_decode($result[1], true);
$payload = $responseJson['request_data'] ?? [];
if (empty($payload)) {
throw new \Exception("빈 페이로드 데이터");
}
// 서비스의 함수 하나로 모든 처리 완료
$insertId = $naverService->processArticle($payload);
// [3] 성공 시 로그 업데이트 (재연결 처리 포함)
$this->safeUpdateLog($logModel, $logId, [
'atcl_no' => $payload['articleNumber'] ?? null,
'status' => 'SUCCESS',
'target_db_id' => $insertId
]);
// [1] 꺼내자마자 DB에 원문 저장 (2차 임시 저장) - 실패 시 재시도
try {
$logId = $logModel->insert([
'raw_payload' => $rawData,
'status' => 'INIT'
]);
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
// MySQL gone away 에러 시 재연결 후 재시도
if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) {
CLI::write(CLI::color('⚠️ MySQL gone away, reconnecting...', 'yellow'));
$this->db->close();
$this->db = \Config\Database::connect();
$logModel = model(NaverWorkerLogModel::class);
CLI::write("✅ Success! DB ID: $insertId", 'cyan');
} catch (\Exception $e) {
CLI::error("❌ Task Failed: " . $e->getMessage());
// 실패 로그는 여기서 남김
// 1. DB 상태를 FAIL로 업데이트 (필수) (재연결 처리 포함)
$this->safeUpdateLog($logModel, $logId, [
'status' => 'FAIL',
'error_msg' => $e->getMessage()
// 재시도
$logId = $logModel->insert([
'raw_payload' => $rawData,
'status' => 'INIT'
]);
// 2. Redis 실패 큐에 백업 (선택 - 나중에 모아서 다시 던질 때 편함)
$redis->lPush('naver:failed_queue', $rawData);
helper('log');
write_custom_log("FAILED_DATA | Error: " . $e->getMessage(), 'ERROR', 'failed');
// 루프 과부하 방지 (연속 에러 시)
sleep(1);
} else {
throw $e; // 다른 에러면 그대로 throw
}
}
try {
$responseJson = json_decode($rawData, true);
$payload = $responseJson['request_data'] ?? [];
if (empty($payload)) {
throw new \Exception("빈 페이로드 데이터");
}
// 서비스의 함수 하나로 모든 처리 완료
$insertId = $naverService->processArticle($payload);
// [3] 성공 시 로그 업데이트 (재연결 처리 포함)
$this->safeUpdateLog($logModel, $logId, [
'atcl_no' => $payload['articleNumber'] ?? null,
'status' => 'SUCCESS',
'target_db_id' => $insertId
]);
CLI::write("✅ Success! DB ID: $insertId | Source: $source", 'cyan');
} catch (\Exception $e) {
CLI::error("❌ Task Failed: " . $e->getMessage());
// 실패 로그는 여기서 남김
// 1. DB 상태를 FAIL로 업데이트 (필수) (재연결 처리 포함)
$this->safeUpdateLog($logModel, $logId, [
'status' => 'FAIL',
'error_msg' => $e->getMessage()
]);
// 2. Redis 실패 큐에 백업 (선택 - Redis가 있을 경우만)
if ($redis) {
try {
$redis->lPush('naver:failed_queue', $rawData);
} catch (\Exception $redisEx) {
// Redis 실패 시에도 에러 처리하지 않음 (이미 DB에 FAIL 로그 남김)
CLI::write(CLI::color('⚠️ Failed to push to failed_queue: ' . $redisEx->getMessage(), 'yellow'));
}
}
helper('log');
write_custom_log("FAILED_DATA | Error: " . $e->getMessage(), 'ERROR', 'failed');
// 루프 과부하 방지 (연속 에러 시)
sleep(1);
}
}
}
}
/**
* MySQL gone away 에러 발생 시 재연결 후 재시도하는 안전한 update
@@ -176,5 +195,68 @@ class NaverWorker extends BaseCommand
}
}
/**
* 폴백 파일에서 데이터 읽기 (Redis 장애 시 파일에서 직접 처리)
*
* @return string|null JSON 데이터 또는 null
*/
protected function readFromFallbackFile()
{
$fallbackDir = ROOTPATH . 'worker/fallback_queue';
// 폴백 디렉토리가 없으면 null 반환
if (!is_dir($fallbackDir)) {
return null;
}
// 폴백 파일 목록 가져오기 (오래된 순서대로)
$files = glob($fallbackDir . '/*.json');
if (empty($files)) {
return null;
}
sort($files); // 파일명(타임스탬프) 기준 정렬
// 가장 오래된 파일 하나 처리
$filePath = $files[0];
try {
// 파일 락을 사용하여 읽기 (동시 접근 방지)
$fp = fopen($filePath, 'r');
if (!$fp) {
CLI::write(CLI::color("⚠️ Failed to open fallback file: " . basename($filePath), 'yellow'));
return null;
}
// 배타적 락 획득 시도
if (!flock($fp, LOCK_EX | LOCK_NB)) {
// 락 획득 실패 (다른 프로세스가 처리 중)
fclose($fp);
return null;
}
// 파일 내용 읽기
$content = stream_get_contents($fp);
// 파일 삭제 (처리 완료로 간주)
flock($fp, LOCK_UN);
fclose($fp);
unlink($filePath);
CLI::write(CLI::color("📂 Processing fallback file: " . basename($filePath), 'green'));
return $content;
} catch (\Exception $e) {
CLI::write(CLI::color("❌ Error reading fallback file " . basename($filePath) . ": " . $e->getMessage(), 'red'));
if (isset($fp) && is_resource($fp)) {
flock($fp, LOCK_UN);
fclose($fp);
}
return null;
}
}
}

View File

@@ -4,7 +4,7 @@ namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Session\Handlers\BaseHandler;
use CodeIgniter\Session\Handlers\FileHandler;
use CodeIgniter\Session\Handlers\DatabaseHandler;
use CodeIgniter\Session\Handlers\RedisHandler;
class Session extends BaseConfig
@@ -15,14 +15,11 @@ class Session extends BaseConfig
* --------------------------------------------------------------------------
*
* The session storage driver to use:
* - `CodeIgniter\Session\Handlers\FileHandler`
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
* - `CodeIgniter\Session\Handlers\RedisHandler`
*
* @var class-string<BaseHandler>
*/
// public string $driver = FileHandler::class;
public string $driver = RedisHandler::class;
/**
@@ -51,43 +48,59 @@ class Session extends BaseConfig
*
* The location to save sessions to and is driver dependent.
*
* For the 'files' driver, it's a path to a writable directory.
* WARNING: Only absolute paths are supported!
*
* For the 'database' driver, it's a table name.
* Please read up the manual for the format with other session drivers.
* For Redis: tcp://host:port?database=n&password=xxx
*
* IMPORTANT: You are REQUIRED to set a valid save path!
*
* For Redis: tcp://host:port?database=n&password=xxx
*/
// public string $savePath = WRITEPATH . 'session';
public string $savePath;
public function __construct()
{
parent::__construct();
// Redis 설정: .env 우선, 없으면 Docker 환경변수 사용
if ($this->driver === RedisHandler::class) {
// .env 파일이 우선, 없으면 Docker compose 환경변수 사용
$redisHost = env('SESSION_REDIS_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1';
$redisPort = env('SESSION_REDIS_PORT') ?: getenv('REDIS_PORT') ?: '6379';
$redisDatabase = env('SESSION_REDIS_DATABASE') ?: getenv('REDIS_DATABASE') ?: '0';
$redisPassword = env('SESSION_REDIS_PASSWORD') ?: getenv('REDIS_PASSWORD') ?: '';
// 환경변수로 강제로 Database 모드 설정 가능 (Redis 장애 시 수동 전환)
$forceDatabase = env('SESSION_FORCE_DATABASE', false);
$this->savePath = sprintf(
'tcp://%s:%s?database=%s',
$redisHost,
$redisPort,
$redisDatabase
);
// Redis 설정: Redis 연결 실패 시 Database로 폴백
if ($this->driver === RedisHandler::class && !$forceDatabase) {
helper('redis');
try {
// Redis 연결 테스트
$testRedis = get_redis_connection('session');
if (!$testRedis) {
throw new \Exception('Redis connection failed');
}
// Redis 정상 - Redis 설정 사용
$config = get_redis_config('session');
$this->savePath = sprintf(
'tcp://%s:%s?database=%s',
$config['host'],
$config['port'],
$config['database']
);
if (!empty($redisPassword)) {
$this->savePath .= '&password=' . $redisPassword;
if (!empty($config['password'])) {
$this->savePath .= '&password=' . $config['password'];
}
} catch (\Exception $e) {
// Redis 실패 - DatabaseHandler로 폴백
log_message('warning', 'Session: Redis unavailable (' . $e->getMessage() . '), falling back to DatabaseHandler');
$this->driver = DatabaseHandler::class;
$this->savePath = 'ci_sessions'; // 테이블 이름
}
} else {
$this->savePath = WRITEPATH . 'session';
// Database 강제 모드 또는 기본 Database 설정
if ($forceDatabase && $this->driver === RedisHandler::class) {
log_message('info', 'Session: Forced to use DatabaseHandler (SESSION_FORCE_DATABASE=true)');
$this->driver = DatabaseHandler::class;
}
$this->savePath = 'ci_sessions';
}
}

View File

@@ -13,6 +13,23 @@ class KisoController extends BaseController
/** @var ResponseInterface */
protected $response;
/**
* 네이버 검증 요청 API
*
* Required Parameters:
* - articleNumber: 기사 번호
* - requestType: 요청 타입 (verify, check, validate)
* - requestDatetime: 요청 일시 (YYYY-MM-DD HH:MM:SS)
*
* Error Codes:
* - E001: 필수 파라미터 누락
* - E002: requestType 값 오류
* - E003: requestDatetime 형식 오류
* - E005: HTTP 메소드 오류
* - E999: Redis 연결 오류
*
* @return ResponseInterface
*/
public function vrfcReq()
{
// 1. 요청 방식에 따라 데이터 파싱
@@ -25,56 +42,79 @@ class KisoController extends BaseController
} else {
// 지원하지 않는 메소드 처리 (예: PUT, DELETE 등)
return $this->response->setStatusCode(Response::HTTP_METHOD_NOT_ALLOWED)
->setJSON(['resultCode' => 'E005', 'resultMessage' => 'Method not allowed. Use GET or POST.']);
->setJSON(['code' => 'E005', 'message' => 'Method not allowed. Use GET or POST.']);
}
// 2. 필수 항목 검증
$requiredKeys = ['articleNumber', 'requestType', 'requestDatetime'];
foreach ($requiredKeys as $key) {
// 파싱된 데이터($data) 내에 키가 없거나 값이 비어있는지 확인
if (empty($data[$key])) {
// isset()과 trim()을 사용하여 '0' 값도 허용
if (!isset($data[$key]) || trim((string)$data[$key]) === '') {
return $this->response->setStatusCode(Response::HTTP_BAD_REQUEST)
->setJSON([
'resultCode' => 'E001',
'resultMessage' => "Missing required parameter: {$key}"
'code' => 'E001',
'message' => "Missing required parameter: {$key}"
]);
}
}
// 3. Redis 연결 및 예외 처리
// 3. Redis 연결 및 직접 푸시
try {
$redis = new \Redis();
// Docker 환경이므로 host를 'redis'로 설정
$success = $redis->connect('redis', 6379);
if (!$success) {
// 3. requestType 값 검증
$validRequestTypes = ['verify', 'check', 'validate']; // 허용되는 requestType 값
if (!in_array($data['requestType'], $validRequestTypes, true)) {
return $this->response->setStatusCode(Response::HTTP_BAD_REQUEST)
->setJSON([
'code' => 'E002',
'message' => "Invalid requestType. Allowed values: " . implode(', ', $validRequestTypes)
]);
}
// 4. requestDatetime 날짜 형식 검증 (Y-m-d H:i:s 형식)
$datetime = \DateTime::createFromFormat('Y-m-d H:i:s', $data['requestDatetime']);
if (!$datetime || $datetime->format('Y-m-d H:i:s') !== $data['requestDatetime']) {
return $this->response->setStatusCode(Response::HTTP_BAD_REQUEST)
->setJSON([
'code' => 'E003',
'message' => "Invalid requestDatetime format. Use: YYYY-MM-DD HH:MM:SS"
]);
}
// 5. Redis 연결 및 큐 저장
helper('redis');
try {
$redis = get_redis_connection('worker');
if (!$redis) {
throw new \Exception('Redis connection failed');
}
$redis->select(10); // 10번 DB 선택
// 데이터 준비
$data['retry_count'] = 0;
$data['received_at'] = date('Y-m-d H:i:s');
// 리스트에 데이터 삽입 (이 명령어가 실행되어야 monitor에 LPUSH가 뜹니다)
$redis->lPush('naver:queue', json_encode($data));
// 리스트에 데이터 삽입
$pushResult = $redis->lPush('naver:queue', json_encode($data));
if (!$pushResult) {
throw new \Exception('Failed to push data to Redis queue');
}
// 성공 로그 기록
log_message('info', "Request queued successfully - Article: {$data['articleNumber']}, Type: {$data['requestType']}");
} catch (\Exception $e) {
log_message('error', 'Redis Push Error: ' . $e->getMessage());
return $this->response->setStatusCode(500)->setJSON([
'resultCode' => 'E999',
'resultMessage' => 'Redis Connection Error'
'code' => 'E999',
'message' => 'Redis Connection Error'
]);
}
// 4. 성공 응답
return $this->response->setStatusCode(Response::HTTP_ACCEPTED) // 202 Accepted
// 6. 성공 응답
return $this->response->setStatusCode(Response::HTTP_OK) // 200 OK
->setJSON([
'resultCode' => 'S000',
'resultMessage' => 'Request successfully queued for processing',
'articleNumber' => $data['articleNumber']
'code' => 'success',
'message' => ''
]);
}
}

View File

@@ -3,6 +3,7 @@ namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\common\LoginModel;
use CodeIgniter\Session\Handlers\DatabaseHandler;
class Login extends BaseController
{
@@ -13,6 +14,37 @@ class Login extends BaseController
$this->loginModel = new LoginModel();
}
/**
* Redis 폴백 상태 체크 (세션이 DatabaseHandler로 동작 중인지)
*/
private function getSystemStatus(): array
{
$status = [
'redis_fallback' => false,
'warning_message' => ''
];
// Session 설정에서 현재 드라이버 확인
$sessionConfig = config('Session');
if ($sessionConfig->driver === DatabaseHandler::class) {
$status['redis_fallback'] = true;
$status['warning_message'] = '세션 서버(Redis) 장애로 임시 모드로 운영 중입니다. 시스템 관리자에게 문의하세요.';
}
return $status;
}
/**
* JSON 응답에 시스템 상태 추가
*/
private function jsonResponse(array $data): \CodeIgniter\HTTP\ResponseInterface
{
$systemStatus = $this->getSystemStatus();
$data['system'] = $systemStatus;
return $this->response->setJSON($data);
}
public function index(): string
{
$user_id = get_cookie('save_id');
@@ -59,7 +91,7 @@ class Login extends BaseController
];
if (!$this->validate($rules)) {
return $this->response->setJSON([
return $this->jsonResponse([
'code' => '1',
'errors' => $this->validator->getErrors()
]);
@@ -82,7 +114,7 @@ class Login extends BaseController
$this->loginModel->insertUserLog($logs);
return $this->response->setJSON([
return $this->jsonResponse([
'code' => '1',
'msg' => '존재하지 않는 아이디입니다.'
]);
@@ -96,7 +128,7 @@ class Login extends BaseController
$this->loginModel->insertUserLog($logs);
return $this->response->setJSON(body: [
return $this->jsonResponse([
'code' => '1',
'msg' => '잘못된 비밀번호 입니다.'
]);
@@ -135,7 +167,7 @@ class Login extends BaseController
$this->session->set($newdata);
return $this->response->setJSON([
return $this->jsonResponse([
'code' => '0',
'msg' => 'success'
]);
@@ -148,7 +180,17 @@ class Login extends BaseController
log_message('error', '[LOGIN ERROR] ' . $e->getMessage());
log_message('error', $e->getTraceAsString());
return $this->response->setJSON([
// 세션 관련 에러인지 확인
$errorMessage = $e->getMessage();
if (stripos($errorMessage, 'session') !== false ||
stripos($errorMessage, 'redis') !== false) {
return $this->jsonResponse([
'code' => '8',
'msg' => '세션 서비스 오류입니다. 시스템 관리자에게 문의해주세요.'
]);
}
return $this->jsonResponse([
'code' => '9',
'msg' => '서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'
]);

View File

@@ -13,10 +13,31 @@ class AuthCheck implements FilterInterface
$session = session();
log_message('debug', 'URI PATH: ' . service('uri')->getPath());
// 로그인 체크
if (!$session->get('logged_in')) {
// 로그인 안 되어 있으면 로그인 페이지로
return redirect()->to('/login');
try {
// 세션 읽기 시도
$loggedIn = $session->get('logged_in');
// 로그인 체크
if (!$loggedIn) {
// 로그인 안 되어 있으면 로그인 페이지로
return redirect()->to('/login');
}
} catch (\Exception $e) {
// 세션 오류 (Redis 다운 등) - 모든 예외 catch
log_message('error', '[SESSION ERROR in AuthCheck] ' . $e->getMessage());
// AJAX 요청인 경우 JSON 응답
if ($request->isAJAX()) {
return service('response')
->setStatusCode(503)
->setJSON([
'code' => '8',
'msg' => '세션 서비스 오류입니다. 페이지를 새로고침 해주세요. (Redis)'
]);
}
// 일반 요청인 경우 로그인 페이지로 리다이렉트 (에러 메시지 포함)
return redirect()->to('/login')->with('error', '세션 서비스 오류입니다. 시스템 관리자에게 문의해주세요.');
}
}

View File

@@ -0,0 +1,82 @@
<?php
if (!function_exists('get_redis_connection')) {
/**
* Redis 연결을 반환하는 헬퍼 함수
* .env 환경변수에서 설정을 읽어 Redis에 연결합니다.
*
* @param string $type 'worker' 또는 'session' (기본값: 'worker')
* @return Redis|false Redis 객체 또는 연결 실패 시 false
* @throws RedisException
*/
function get_redis_connection(string $type = 'worker')
{
$redis = new \Redis();
try {
// 타입에 따라 다른 환경변수 사용
if ($type === 'session') {
$host = env('SESSION_REDIS_HOST', '127.0.0.1');
$port = env('SESSION_REDIS_PORT', '6379');
$database = env('SESSION_REDIS_DATABASE', '0');
$password = env('SESSION_REDIS_PASSWORD', '');
} else {
// worker, default
$host = env('REDIS_HOST', '127.0.0.1');
$port = env('REDIS_PORT', '6379');
$database = env('REDIS_DATABASE', '9');
$password = env('REDIS_PASSWORD', '');
}
// Redis 연결 (타임아웃: 0.5초)
$timeout = 0.5;
$success = $redis->connect($host, (int)$port, $timeout);
if (!$success) {
log_message('error', "Redis connection failed: {$host}:{$port}");
return false;
}
// 비밀번호 인증 (있는 경우)
if (!empty($password)) {
$redis->auth($password);
}
// 데이터베이스 선택
$redis->select((int)$database);
return $redis;
} catch (\RedisException $e) {
log_message('error', "Redis connection error: " . $e->getMessage());
return false;
}
}
}
if (!function_exists('get_redis_config')) {
/**
* Redis 설정 정보를 배열로 반환
*
* @param string $type 'worker' 또는 'session'
* @return array Redis 설정 배열
*/
function get_redis_config(string $type = 'worker'): array
{
if ($type === 'session') {
return [
'host' => env('SESSION_REDIS_HOST', '127.0.0.1'),
'port' => env('SESSION_REDIS_PORT', '6379'),
'database' => env('SESSION_REDIS_DATABASE', '0'),
'password' => env('SESSION_REDIS_PASSWORD', ''),
];
}
return [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DATABASE', '9'),
'password' => env('REDIS_PASSWORD', ''),
];
}
}

View File

@@ -53,9 +53,17 @@ class TypeSHandler
// 2. Result 데이터 저장
$resultData = $this->parameterMapper->mapResult($rcptSq, $rawData);
write_custom_log("Result Insert 데이터: " . json_encode($resultData, JSON_UNESCAPED_UNICODE), 'INFO', 'service');
if (!$this->resultModel->insert($resultData)) {
throw new Exception("Result Insert 실패");
$lastQuery = (string)$this->resultModel->getLastQuery();
$errors = json_encode($this->resultModel->errors());
write_custom_log("Result Insert 실패 | SQL: $lastQuery | Errors: $errors", 'ERROR', 'service');
throw new Exception("Result Insert 실패: $errors");
}
$insertedResultSql = (string)$this->resultModel->getLastQuery();
write_custom_log("Result Insert 성공 | SQL: $insertedResultSql", 'INFO', 'service');
CLI::write(CLI::color('✅ Result 저장 성공', 'blue'));
// 3. 트랜잭션 커밋

View File

@@ -115,21 +115,39 @@ class TypeSParameterMapper extends BaseParameterMapper
$charge = null;
if ($sectorNumber) {
$charge = $this->regionModel->getChargeByRegionCd($sectorNumber);
log_message('info', "[TypeSParameterMapper] 지역코드: {$sectorNumber}, 조회 결과: " . json_encode($charge, JSON_UNESCAPED_UNICODE));
}
// 2. 기본 담당자 설정 (지역별 담당자가 없으면)
$deptSq = $charge['dept_sq'] ?? '26';
$usrSq = $charge['usr_sq'] ?? '1';
// 담당자 존재 여부 검증 (users 테이블 확인)
if ($usrSq && $usrSq !== '1') {
$db = \Config\Database::connect();
$userExists = $db->table('users')->where('usr_sq', $usrSq)->countAllResults() > 0;
if (!$userExists) {
log_message('warning', "[TypeSParameterMapper] usr_sq={$usrSq}가 users 테이블에 없음. 기본값(1)으로 폴백");
$usrSq = '1';
$deptSq = '26';
}
}
log_message('info', "[TypeSParameterMapper] 기본 담당자 - dept_sq: {$deptSq}, usr_sq: {$usrSq}, VR검증: " . ($isVrVerification ? 'Y' : 'N'));
// 3. VR 검증인 경우 환경별 담당자로 덮어쓰기
if ($isVrVerification) {
log_message('info', "[TypeSParameterMapper] ENVIRONMENT 상수 값: " . ENVIRONMENT);
if (ENVIRONMENT === 'development') {
$deptSq = '29';
$usrSq = '472';
$usrSq = '472'; // vradmin
log_message('info', "[TypeSParameterMapper] VR 검증 (DEV) - dept_sq: {$deptSq}, usr_sq: {$usrSq}");
} else {
// production
$deptSq = '33';
$usrSq = '1993';
// production - TODO: 실제 프로덕션 VR 담당자로 변경 필요
$deptSq = '29';
$usrSq = '472'; // 임시: vradmin (프로덕션 VR 담당자 확인 후 수정)
log_message('warning', "[TypeSParameterMapper] VR 검증 (PROD) - 프로덕션 VR 담당자 미설정, 임시 담당자 사용: usr_sq={$usrSq}");
}
}

View File

@@ -69,6 +69,15 @@
<div class="h-100 bg-plum-plate bg-animation">
<div class="d-flex h-100 justify-content-center align-items-center py-4">
<div class="mx-auto col-sm-10 col-md-8 col-lg-6 col-xl-5">
<!-- Redis 장애 경고 영역 -->
<div id="redis-warning" class="alert alert-warning alert-dismissible fade d-none mb-3"
style="border-radius: 16px; background: rgba(255, 193, 7, 0.95); backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
<strong><i class="fa fa-exclamation-triangle me-2"></i>시스템 알림</strong>
<p id="redis-warning-message" class="mb-0 mt-2"></p>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div class="card border-0"
style="border-radius: 24px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);">
<div class="card-body p-5">
@@ -202,6 +211,27 @@
const tpl = document.querySelector('.my-loader-template');
var table;
// Redis 장애 경고 표시 함수
function showRedisWarning(message) {
const warningDiv = $('#redis-warning');
const messageEl = $('#redis-warning-message');
messageEl.text(message);
warningDiv.removeClass('d-none').addClass('show');
// 10초 후 자동 숨김 (선택사항)
setTimeout(function() {
warningDiv.fadeOut();
}, 15000);
}
// 시스템 상태 체크 함수
function checkSystemStatus(responseData) {
if (responseData.system && responseData.system.redis_fallback) {
showRedisWarning(responseData.system.warning_message);
}
}
$(function () {
$("#btnSearch").on("click", function () {
@@ -231,6 +261,9 @@
console.log(xhr.responseText);
},
success: function (result) {
// 시스템 상태 체크 (Redis 장애 여부)
checkSystemStatus(result);
if (result.code === "0") {
location.href = '/'
} else {