worker service 매물 등록 및 redis 다운시 처리
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -175,4 +175,4 @@ _modules/*
|
|||||||
|
|
||||||
|
|
||||||
# 6. 기타 개인 설정 파일 (선택적)
|
# 6. 기타 개인 설정 파일 (선택적)
|
||||||
.github/copilot-instructions.md
|
.github/copilot-instructions.mdworker/fallback_queue/*.json
|
||||||
|
|||||||
360
SESSION_README.md
Normal file
360
SESSION_README.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# Session 관리 가이드
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
본 애플리케이션은 **Redis**를 기본 세션 저장소로 사용하며, Redis 장애 시 **Database(MariaDB)**로 자동 폴백하는 이중화 구조를 가지고 있습니다.
|
||||||
|
|
||||||
|
## 아키텍처
|
||||||
|
|
||||||
|
### 세션 저장소
|
||||||
|
|
||||||
|
| 우선순위 | 핸들러 | 설명 | 성능 |
|
||||||
|
|---------|-------|------|------|
|
||||||
|
| 1순위 | **RedisHandler** | 메모리 기반 고속 세션 | ~0.03초 |
|
||||||
|
| 2순위 | **DatabaseHandler** | DB 기반 안정적 세션 | ~0.05초 (수동 전환 시) |
|
||||||
|
|
||||||
|
### 구성 요소
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 사용자 로그인 요청 │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Session.php 생성자 │
|
||||||
|
│ - Redis 연결 테스트 │
|
||||||
|
│ - SESSION_FORCE_DATABASE 확인 │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────┴──────┐
|
||||||
|
│ │
|
||||||
|
Redis 정상? Redis 장애?
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
RedisHandler DatabaseHandler
|
||||||
|
(ci_sessions (ci_sessions
|
||||||
|
테이블) 테이블)
|
||||||
|
│ │
|
||||||
|
└──────┬──────┘
|
||||||
|
▼
|
||||||
|
세션 데이터 저장/조회
|
||||||
|
```
|
||||||
|
|
||||||
|
## 설정 파일
|
||||||
|
|
||||||
|
### 1. Session.php (`src/app/Config/Session.php`)
|
||||||
|
|
||||||
|
```php
|
||||||
|
public string $driver = RedisHandler::class;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// 환경변수로 강제 Database 모드 설정 가능
|
||||||
|
$forceDatabase = env('SESSION_FORCE_DATABASE', false);
|
||||||
|
|
||||||
|
if ($this->driver === RedisHandler::class && !$forceDatabase) {
|
||||||
|
try {
|
||||||
|
// Redis 연결 테스트 (타임아웃: 0.5초)
|
||||||
|
$testRedis = get_redis_connection('session');
|
||||||
|
|
||||||
|
if (!$testRedis) {
|
||||||
|
throw new \Exception('Redis connection failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis 정상 - Redis 사용
|
||||||
|
$this->savePath = 'tcp://192.168.10.243:6379?database=0';
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Redis 실패 - DatabaseHandler로 폴백
|
||||||
|
$this->driver = DatabaseHandler::class;
|
||||||
|
$this->savePath = 'ci_sessions';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Database 모드
|
||||||
|
$this->savePath = 'ci_sessions';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 환경 설정 (`.env`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redis 세션 설정
|
||||||
|
SESSION_REDIS_HOST = 192.168.10.243
|
||||||
|
SESSION_REDIS_PORT = 6379
|
||||||
|
SESSION_REDIS_DATABASE = 0
|
||||||
|
#SESSION_REDIS_PASSWORD =
|
||||||
|
|
||||||
|
# 세션 강제 Database 모드 (Redis 장애 시 true로 변경)
|
||||||
|
SESSION_FORCE_DATABASE = false
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Redis Helper (`src/app/Helpers/redis_helper.php`)
|
||||||
|
|
||||||
|
```php
|
||||||
|
function get_redis_connection(string $type = 'worker')
|
||||||
|
{
|
||||||
|
$redis = new \Redis();
|
||||||
|
|
||||||
|
// 타임아웃 0.5초로 빠른 실패 감지
|
||||||
|
$timeout = 0.5;
|
||||||
|
$success = $redis->connect($host, $port, $timeout);
|
||||||
|
|
||||||
|
return $success ? $redis : false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 데이터베이스 테이블
|
||||||
|
|
||||||
|
### ci_sessions 테이블 구조
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `ci_sessions` (
|
||||||
|
`session_id` VARCHAR(40) NOT NULL DEFAULT '0',
|
||||||
|
`ip_address` VARCHAR(16) NOT NULL DEFAULT '0',
|
||||||
|
`user_agent` VARCHAR(120) NOT NULL,
|
||||||
|
`last_activity` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`user_data` VARCHAR(4000) NOT NULL,
|
||||||
|
PRIMARY KEY (`session_id`)
|
||||||
|
) ENGINE = MEMORY;
|
||||||
|
|
||||||
|
CREATE INDEX `last_activity_idx`
|
||||||
|
ON `ci_sessions` (`last_activity` ASC);
|
||||||
|
```
|
||||||
|
|
||||||
|
**주요 특징:**
|
||||||
|
- **ENGINE = MEMORY**: 메모리 기반 테이블로 매우 빠른 읽기/쓰기 성능
|
||||||
|
- **장점**: Redis와 비슷한 속도 (디스크 I/O 없음)
|
||||||
|
- **단점**: 서버 재시작 시 세션 데이터 소실 (일반적으로 문제없음 - 세션은 휘발성 데이터)
|
||||||
|
- **용량**: VARCHAR(4000)으로 충분한 세션 데이터 저장 가능
|
||||||
|
|
||||||
|
## 운영 가이드
|
||||||
|
|
||||||
|
### 1. Redis 장애 발생 시
|
||||||
|
|
||||||
|
#### 자동 폴백 (기본 동작)
|
||||||
|
- Redis 연결 실패 시 자동으로 DatabaseHandler로 전환
|
||||||
|
- **단점**: 매 요청마다 Redis 연결 시도 → 타임아웃 대기 (약 0.5초 추가)
|
||||||
|
- 사용자는 약간 느린 응답 시간을 경험
|
||||||
|
|
||||||
|
#### 수동 전환 (권장)
|
||||||
|
Redis 장애 감지 즉시 다음 조치:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. .env 파일 수정
|
||||||
|
vi /path/to/src/.env
|
||||||
|
|
||||||
|
# SESSION_FORCE_DATABASE를 true로 변경
|
||||||
|
SESSION_FORCE_DATABASE = true
|
||||||
|
|
||||||
|
# 2. PHP-FPM 재시작 (선택사항, 다음 요청부터 자동 적용)
|
||||||
|
docker exec projects-admin_confirms kill -USR2 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**장점**: Redis 연결 시도 없이 즉시 Database 사용 → 빠른 응답 (0.05초)
|
||||||
|
|
||||||
|
### 2. Redis 복구 후
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. .env 파일 원복
|
||||||
|
SESSION_FORCE_DATABASE = false
|
||||||
|
|
||||||
|
# 2. PHP-FPM 재시작
|
||||||
|
docker exec projects-admin_confirms kill -USR2 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 모니터링
|
||||||
|
|
||||||
|
#### 로그 확인
|
||||||
|
```bash
|
||||||
|
# 세션 관련 로그 확인
|
||||||
|
docker exec projects-admin_confirms tail -f /var/www/html/writable/logs/log-$(date +%Y-%m-%d).log | grep -i session
|
||||||
|
|
||||||
|
# Redis 폴백 경고 확인
|
||||||
|
docker exec projects-admin_confirms grep "Redis unavailable" /var/www/html/writable/logs/log-$(date +%Y-%m-%d).log
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 예상 로그 메시지
|
||||||
|
|
||||||
|
**Redis 정상:**
|
||||||
|
```
|
||||||
|
DEBUG - Session: Class initialized using 'CodeIgniter\Session\Handlers\RedisHandler' driver.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Redis 장애 (자동 폴백):**
|
||||||
|
```
|
||||||
|
WARNING - Session: Redis unavailable (Redis connection failed), falling back to DatabaseHandler
|
||||||
|
DEBUG - Session: Class initialized using 'CodeIgniter\Session\Handlers\DatabaseHandler' driver.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database 강제 모드:**
|
||||||
|
```
|
||||||
|
INFO - Session: Forced to use DatabaseHandler (SESSION_FORCE_DATABASE=true)
|
||||||
|
DEBUG - Session: Class initialized using 'CodeIgniter\Session\Handlers\DatabaseHandler' driver.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 성능 모니터링
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 로그인 응답 시간 측정
|
||||||
|
for i in {1..5}; do
|
||||||
|
curl -X POST http://localtest2-admin.confirms.co.kr/login/chkLogin \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-d "user_id=test&user_pw=test" \
|
||||||
|
-o /dev/null -s -w " Time: %{time_total}s\n"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**기대 성능:**
|
||||||
|
- Redis 정상: 0.03 ~ 0.05초
|
||||||
|
- Database (수동 전환): 0.05 ~ 0.08초
|
||||||
|
- 자동 폴백 (Redis 다운): 0.5 ~ 1.0초
|
||||||
|
|
||||||
|
## 사용자 알림
|
||||||
|
|
||||||
|
### 프론트엔드 경고 표시
|
||||||
|
|
||||||
|
Redis 장애 시 로그인 페이지에 자동으로 경고 배너가 표시됩니다:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// login.php의 JavaScript
|
||||||
|
function checkSystemStatus(responseData) {
|
||||||
|
if (responseData.system && responseData.system.redis_fallback) {
|
||||||
|
// 경고 메시지: "세션 서버(Redis) 장애로 임시 모드로 운영 중입니다."
|
||||||
|
showRedisWarning(responseData.system.warning_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**API 응답 예시:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"msg": "success",
|
||||||
|
"system": {
|
||||||
|
"redis_fallback": true,
|
||||||
|
"warning_message": "세션 서버(Redis) 장애로 임시 모드로 운영 중입니다. 시스템 관리자에게 문의하세요."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 트러블슈팅
|
||||||
|
|
||||||
|
### 문제: 로그인이 느림 (2~3초)
|
||||||
|
|
||||||
|
**원인**: Redis 다운 상태에서 자동 폴백 모드 사용 중
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```bash
|
||||||
|
# SESSION_FORCE_DATABASE=true로 수동 전환
|
||||||
|
echo "SESSION_FORCE_DATABASE = true" >> /path/to/src/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문제: 세션이 유지되지 않음
|
||||||
|
|
||||||
|
**원인**: ci_sessions 테이블이 없거나 권한 문제
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```sql
|
||||||
|
-- 테이블 존재 확인
|
||||||
|
SHOW TABLES LIKE 'ci_sessions';
|
||||||
|
|
||||||
|
-- 권한 확인
|
||||||
|
SHOW GRANTS FOR 'confirms'@'%';
|
||||||
|
|
||||||
|
-- 테이블이 없는 경우 생성
|
||||||
|
CREATE TABLE `ci_sessions` (
|
||||||
|
`session_id` VARCHAR(40) NOT NULL DEFAULT '0',
|
||||||
|
`ip_address` VARCHAR(16) NOT NULL DEFAULT '0',
|
||||||
|
`user_agent` VARCHAR(120) NOT NULL,
|
||||||
|
`last_activity` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`user_data` VARCHAR(4000) NOT NULL,
|
||||||
|
PRIMARY KEY (`session_id`)
|
||||||
|
) ENGINE = MEMORY;
|
||||||
|
|
||||||
|
CREATE INDEX `last_activity_idx`
|
||||||
|
ON `ci_sessions` (`last_activity` ASC);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문제: 서버 재시작 후 모든 세션이 사라짐
|
||||||
|
|
||||||
|
**원인**: ci_sessions가 MEMORY 엔진을 사용 (정상 동작)
|
||||||
|
|
||||||
|
**설명**:
|
||||||
|
- MEMORY 엔진은 서버 재시작 시 데이터가 삭제됩니다
|
||||||
|
- 세션은 일시적 데이터이므로 일반적으로 문제가 되지 않습니다
|
||||||
|
- 사용자는 재로그인하면 됩니다
|
||||||
|
|
||||||
|
**영구 저장이 필요한 경우**:
|
||||||
|
```sql
|
||||||
|
-- InnoDB로 변경 (성능은 약간 느려짐)
|
||||||
|
ALTER TABLE ci_sessions ENGINE=InnoDB;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문제: Redis 연결 타임아웃이 너무 김
|
||||||
|
|
||||||
|
**원인**: redis_helper.php의 타임아웃 설정
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```php
|
||||||
|
// src/app/Helpers/redis_helper.php
|
||||||
|
$timeout = 0.5; // 0.1 ~ 1.0 사이 값으로 조정
|
||||||
|
$redis->connect($host, $port, $timeout);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 관련 파일
|
||||||
|
|
||||||
|
| 파일 | 역할 |
|
||||||
|
|------|------|
|
||||||
|
| `src/app/Config/Session.php` | 세션 설정 및 폴백 로직 |
|
||||||
|
| `src/app/Helpers/redis_helper.php` | Redis 연결 헬퍼 함수 |
|
||||||
|
| `src/app/Controllers/Login.php` | 로그인 및 시스템 상태 체크 |
|
||||||
|
| `src/app/Views/pages/login.php` | 프론트엔드 경고 표시 |
|
||||||
|
| `src/.env` | 환경 설정 (Redis 연결 정보, 강제 모드) |
|
||||||
|
|
||||||
|
## Worker 및 API (참고)
|
||||||
|
|
||||||
|
### Worker (NaverWorker.php)
|
||||||
|
- Redis 큐 사용 (DB 9)
|
||||||
|
- Redis 장애 시 파일 기반 폴백 (`worker/fallback_queue/*.json`)
|
||||||
|
|
||||||
|
### API (api_receiver.php, KisoController.php)
|
||||||
|
- Redis 큐 사용 (DB 9)
|
||||||
|
- Redis 장애 시 파일 기반 폴백 (`worker/fallback_queue/*.json`)
|
||||||
|
|
||||||
|
## 추가 개선 사항
|
||||||
|
|
||||||
|
### 고려 중인 기능
|
||||||
|
|
||||||
|
1. **Redis Sentinel 도입** (고가용성)
|
||||||
|
- 자동 장애 조치
|
||||||
|
- 마스터/슬레이브 구조
|
||||||
|
|
||||||
|
2. **APM 연동**
|
||||||
|
- New Relic, DataDog 등
|
||||||
|
- 실시간 성능 모니터링
|
||||||
|
|
||||||
|
3. **헬스체크 엔드포인트**
|
||||||
|
```php
|
||||||
|
// GET /health/session
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"driver": "RedisHandler",
|
||||||
|
"redis_available": true,
|
||||||
|
"response_time_ms": 12
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 문의
|
||||||
|
|
||||||
|
세션 관련 문제 발생 시:
|
||||||
|
1. 로그 확인 (`/writable/logs/`)
|
||||||
|
2. Redis 서버 상태 확인
|
||||||
|
3. ci_sessions 테이블 상태 확인
|
||||||
|
4. 필요 시 수동 전환 수행
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2026-03-25
|
||||||
|
**Version**: 1.0.0
|
||||||
@@ -23,46 +23,50 @@ class NaverWorker extends BaseCommand
|
|||||||
|
|
||||||
public function run(array $params)
|
public function run(array $params)
|
||||||
{
|
{
|
||||||
helper('log'); // 여기서 로드 완료!
|
helper(['log', 'redis']); // redis helper 추가
|
||||||
|
|
||||||
$this->db = \Config\Database::connect();
|
$this->db = \Config\Database::connect();
|
||||||
$logModel = model(NaverWorkerLogModel::class);
|
$logModel = model(NaverWorkerLogModel::class);
|
||||||
$naverService = new \App\Services\NaverService(); // 서비스 생성
|
$naverService = new \App\Services\NaverService(); // 서비스 생성
|
||||||
|
|
||||||
$redis = new \Redis();
|
// Redis 연결 (실패해도 계속 진행 - 파일 모드로 동작 가능)
|
||||||
try {
|
$redis = get_redis_connection('worker');
|
||||||
// 두 가지 환경 변수 형식 지원 (REDIS_HOST 또는 redis.default.host)
|
$config = get_redis_config('worker');
|
||||||
$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);
|
if ($redis) {
|
||||||
$redis->select((int)$this->redisDatabase);
|
CLI::write(CLI::color('🟢 Naver Worker running... (Redis: ' . $config['host'] . ':' . $config['port'] . ' DB:' . $config['database'] . ')', 'green'));
|
||||||
CLI::write(CLI::color('🟢 Naver Worker running... (Redis: ' . $this->redisHost . ':' . $this->redisPort . ' DB:' . $this->redisDatabase . ')', 'green'));
|
} else {
|
||||||
} catch (\Exception $e) {
|
CLI::write(CLI::color('⚠️ Naver Worker running in FILE-ONLY mode (Redis unavailable)', 'yellow'));
|
||||||
CLI::error("Redis 연결 불가: " . $e->getMessage());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
// Redis brPop with retry logic
|
// Redis 또는 폴백 파일에서 데이터 읽기
|
||||||
$maxRetries = 3;
|
$rawData = null;
|
||||||
|
$source = 'redis'; // 데이터 소스 추적
|
||||||
|
|
||||||
|
// 1. Redis에서 데이터 읽기 시도 (Redis가 있을 경우만)
|
||||||
|
if ($redis) {
|
||||||
|
$maxRetries = 2;
|
||||||
$retryCount = 0;
|
$retryCount = 0;
|
||||||
$result = null;
|
|
||||||
|
|
||||||
while ($retryCount < $maxRetries) {
|
while ($retryCount < $maxRetries) {
|
||||||
try {
|
try {
|
||||||
$result = $redis->brPop(['naver:raw_queue'], 30);
|
$result = $redis->brPop(['naver:raw_queue'], 5); // 5초 타임아웃
|
||||||
|
if ($result) {
|
||||||
|
$rawData = $result[1];
|
||||||
|
$source = 'redis';
|
||||||
|
}
|
||||||
break; // 성공하면 루프 탈출
|
break; // 성공하면 루프 탈출
|
||||||
} catch (\RedisException $e) {
|
} catch (\Exception $e) {
|
||||||
$retryCount++;
|
$retryCount++;
|
||||||
CLI::write(CLI::color("⚠️ Redis error (attempt {$retryCount}/{$maxRetries}): " . $e->getMessage(), 'yellow'));
|
CLI::write(CLI::color("⚠️ Redis error (attempt {$retryCount}/{$maxRetries}): " . $e->getMessage(), 'yellow'));
|
||||||
|
|
||||||
if ($retryCount >= $maxRetries) {
|
if ($retryCount >= $maxRetries) {
|
||||||
CLI::error("❌ Redis reconnection failed after {$maxRetries} attempts");
|
CLI::write(CLI::color("⚠️ Redis unavailable, switching to file mode", 'yellow'));
|
||||||
|
$redis = null; // Redis를 비활성화
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,24 +74,32 @@ class NaverWorker extends BaseCommand
|
|||||||
try {
|
try {
|
||||||
CLI::write(CLI::color('🔄 Reconnecting to Redis...', 'yellow'));
|
CLI::write(CLI::color('🔄 Reconnecting to Redis...', 'yellow'));
|
||||||
$redis->close();
|
$redis->close();
|
||||||
$redis->connect($this->redisHost, (int)$this->redisPort);
|
$redis = get_redis_connection('worker');
|
||||||
$redis->select((int)$this->redisDatabase);
|
if ($redis) {
|
||||||
CLI::write(CLI::color('✅ Redis reconnected', 'green'));
|
CLI::write(CLI::color('✅ Redis reconnected', 'green'));
|
||||||
|
}
|
||||||
} catch (\Exception $reconnectError) {
|
} catch (\Exception $reconnectError) {
|
||||||
CLI::error("Redis reconnection error: " . $reconnectError->getMessage());
|
CLI::error("Redis reconnection error: " . $reconnectError->getMessage());
|
||||||
sleep(2); // 재연결 실패 시 잠시 대기
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$result) {
|
// 2. Redis에서 데이터 없으면 폴백 파일 확인
|
||||||
// 데이터가 없어서 타임아웃 난 경우.
|
if (!$rawData) {
|
||||||
// 굳이 sleep 안 해도 바로 다음 brPop이 다시 30초 대기를 시작함.
|
$rawData = $this->readFromFallbackFile();
|
||||||
|
if ($rawData) {
|
||||||
|
$source = 'file';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 데이터 없으면 다음 루프
|
||||||
|
if (!$rawData) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result) {
|
// 4. 데이터 소스 로깅
|
||||||
$rawData = $result[1];
|
CLI::write(CLI::color("📥 Data received from: " . strtoupper($source), 'cyan'));
|
||||||
|
|
||||||
// [1] 꺼내자마자 DB에 원문 저장 (2차 임시 저장) - 실패 시 재시도
|
// [1] 꺼내자마자 DB에 원문 저장 (2차 임시 저장) - 실패 시 재시도
|
||||||
try {
|
try {
|
||||||
@@ -114,7 +126,7 @@ class NaverWorker extends BaseCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$responseJson = json_decode($result[1], true);
|
$responseJson = json_decode($rawData, true);
|
||||||
$payload = $responseJson['request_data'] ?? [];
|
$payload = $responseJson['request_data'] ?? [];
|
||||||
|
|
||||||
if (empty($payload)) {
|
if (empty($payload)) {
|
||||||
@@ -131,7 +143,7 @@ class NaverWorker extends BaseCommand
|
|||||||
'target_db_id' => $insertId
|
'target_db_id' => $insertId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CLI::write("✅ Success! DB ID: $insertId", 'cyan');
|
CLI::write("✅ Success! DB ID: $insertId | Source: $source", 'cyan');
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
CLI::error("❌ Task Failed: " . $e->getMessage());
|
CLI::error("❌ Task Failed: " . $e->getMessage());
|
||||||
@@ -142,8 +154,16 @@ class NaverWorker extends BaseCommand
|
|||||||
'error_msg' => $e->getMessage()
|
'error_msg' => $e->getMessage()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. Redis 실패 큐에 백업 (선택 - 나중에 모아서 다시 던질 때 편함)
|
// 2. Redis 실패 큐에 백업 (선택 - Redis가 있을 경우만)
|
||||||
|
if ($redis) {
|
||||||
|
try {
|
||||||
$redis->lPush('naver:failed_queue', $rawData);
|
$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');
|
helper('log');
|
||||||
write_custom_log("FAILED_DATA | Error: " . $e->getMessage(), 'ERROR', 'failed');
|
write_custom_log("FAILED_DATA | Error: " . $e->getMessage(), 'ERROR', 'failed');
|
||||||
|
|
||||||
@@ -151,8 +171,7 @@ class NaverWorker extends BaseCommand
|
|||||||
sleep(1);
|
sleep(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MySQL gone away 에러 발생 시 재연결 후 재시도하는 안전한 update
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ namespace Config;
|
|||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
use CodeIgniter\Session\Handlers\BaseHandler;
|
use CodeIgniter\Session\Handlers\BaseHandler;
|
||||||
use CodeIgniter\Session\Handlers\FileHandler;
|
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||||
use CodeIgniter\Session\Handlers\RedisHandler;
|
use CodeIgniter\Session\Handlers\RedisHandler;
|
||||||
|
|
||||||
class Session extends BaseConfig
|
class Session extends BaseConfig
|
||||||
@@ -15,14 +15,11 @@ class Session extends BaseConfig
|
|||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
*
|
*
|
||||||
* The session storage driver to use:
|
* The session storage driver to use:
|
||||||
* - `CodeIgniter\Session\Handlers\FileHandler`
|
|
||||||
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
|
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
|
||||||
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
|
|
||||||
* - `CodeIgniter\Session\Handlers\RedisHandler`
|
* - `CodeIgniter\Session\Handlers\RedisHandler`
|
||||||
*
|
*
|
||||||
* @var class-string<BaseHandler>
|
* @var class-string<BaseHandler>
|
||||||
*/
|
*/
|
||||||
// public string $driver = FileHandler::class;
|
|
||||||
public string $driver = RedisHandler::class;
|
public string $driver = RedisHandler::class;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,43 +48,59 @@ class Session extends BaseConfig
|
|||||||
*
|
*
|
||||||
* The location to save sessions to and is driver dependent.
|
* 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.
|
* 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!
|
* 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 string $savePath;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
// Redis 설정: .env 우선, 없으면 Docker 환경변수 사용
|
// 환경변수로 강제로 Database 모드 설정 가능 (Redis 장애 시 수동 전환)
|
||||||
if ($this->driver === RedisHandler::class) {
|
$forceDatabase = env('SESSION_FORCE_DATABASE', false);
|
||||||
// .env 파일이 우선, 없으면 Docker compose 환경변수 사용
|
|
||||||
$redisHost = env('SESSION_REDIS_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1';
|
// Redis 설정: Redis 연결 실패 시 Database로 폴백
|
||||||
$redisPort = env('SESSION_REDIS_PORT') ?: getenv('REDIS_PORT') ?: '6379';
|
if ($this->driver === RedisHandler::class && !$forceDatabase) {
|
||||||
$redisDatabase = env('SESSION_REDIS_DATABASE') ?: getenv('REDIS_DATABASE') ?: '0';
|
helper('redis');
|
||||||
$redisPassword = env('SESSION_REDIS_PASSWORD') ?: getenv('REDIS_PASSWORD') ?: '';
|
|
||||||
|
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(
|
$this->savePath = sprintf(
|
||||||
'tcp://%s:%s?database=%s',
|
'tcp://%s:%s?database=%s',
|
||||||
$redisHost,
|
$config['host'],
|
||||||
$redisPort,
|
$config['port'],
|
||||||
$redisDatabase
|
$config['database']
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!empty($redisPassword)) {
|
if (!empty($config['password'])) {
|
||||||
$this->savePath .= '&password=' . $redisPassword;
|
$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 {
|
} 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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,23 @@ class KisoController extends BaseController
|
|||||||
/** @var ResponseInterface */
|
/** @var ResponseInterface */
|
||||||
protected $response;
|
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()
|
public function vrfcReq()
|
||||||
{
|
{
|
||||||
// 1. 요청 방식에 따라 데이터 파싱
|
// 1. 요청 방식에 따라 데이터 파싱
|
||||||
@@ -25,56 +42,79 @@ class KisoController extends BaseController
|
|||||||
} else {
|
} else {
|
||||||
// 지원하지 않는 메소드 처리 (예: PUT, DELETE 등)
|
// 지원하지 않는 메소드 처리 (예: PUT, DELETE 등)
|
||||||
return $this->response->setStatusCode(Response::HTTP_METHOD_NOT_ALLOWED)
|
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. 필수 항목 검증
|
// 2. 필수 항목 검증
|
||||||
$requiredKeys = ['articleNumber', 'requestType', 'requestDatetime'];
|
$requiredKeys = ['articleNumber', 'requestType', 'requestDatetime'];
|
||||||
|
|
||||||
foreach ($requiredKeys as $key) {
|
foreach ($requiredKeys as $key) {
|
||||||
// 파싱된 데이터($data) 내에 키가 없거나 값이 비어있는지 확인
|
// isset()과 trim()을 사용하여 '0' 값도 허용
|
||||||
if (empty($data[$key])) {
|
if (!isset($data[$key]) || trim((string)$data[$key]) === '') {
|
||||||
return $this->response->setStatusCode(Response::HTTP_BAD_REQUEST)
|
return $this->response->setStatusCode(Response::HTTP_BAD_REQUEST)
|
||||||
->setJSON([
|
->setJSON([
|
||||||
'resultCode' => 'E001',
|
'code' => 'E001',
|
||||||
'resultMessage' => "Missing required parameter: {$key}"
|
'message' => "Missing required parameter: {$key}"
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Redis 연결 및 예외 처리
|
// 3. requestType 값 검증
|
||||||
// 3. Redis 연결 및 직접 푸시
|
$validRequestTypes = ['verify', 'check', 'validate']; // 허용되는 requestType 값
|
||||||
try {
|
if (!in_array($data['requestType'], $validRequestTypes, true)) {
|
||||||
$redis = new \Redis();
|
return $this->response->setStatusCode(Response::HTTP_BAD_REQUEST)
|
||||||
// Docker 환경이므로 host를 'redis'로 설정
|
->setJSON([
|
||||||
$success = $redis->connect('redis', 6379);
|
'code' => 'E002',
|
||||||
|
'message' => "Invalid requestType. Allowed values: " . implode(', ', $validRequestTypes)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$success) {
|
// 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');
|
throw new \Exception('Redis connection failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
$redis->select(10); // 10번 DB 선택
|
|
||||||
|
|
||||||
// 데이터 준비
|
// 데이터 준비
|
||||||
$data['retry_count'] = 0;
|
$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) {
|
} catch (\Exception $e) {
|
||||||
log_message('error', 'Redis Push Error: ' . $e->getMessage());
|
log_message('error', 'Redis Push Error: ' . $e->getMessage());
|
||||||
return $this->response->setStatusCode(500)->setJSON([
|
return $this->response->setStatusCode(500)->setJSON([
|
||||||
'resultCode' => 'E999',
|
'code' => 'E999',
|
||||||
'resultMessage' => 'Redis Connection Error'
|
'message' => 'Redis Connection Error'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 성공 응답
|
// 6. 성공 응답
|
||||||
return $this->response->setStatusCode(Response::HTTP_ACCEPTED) // 202 Accepted
|
return $this->response->setStatusCode(Response::HTTP_OK) // 200 OK
|
||||||
->setJSON([
|
->setJSON([
|
||||||
'resultCode' => 'S000',
|
'code' => 'success',
|
||||||
'resultMessage' => 'Request successfully queued for processing',
|
'message' => ''
|
||||||
'articleNumber' => $data['articleNumber']
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ namespace App\Controllers;
|
|||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
use App\Models\common\LoginModel;
|
use App\Models\common\LoginModel;
|
||||||
|
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||||
|
|
||||||
class Login extends BaseController
|
class Login extends BaseController
|
||||||
{
|
{
|
||||||
@@ -13,6 +14,37 @@ class Login extends BaseController
|
|||||||
$this->loginModel = new LoginModel();
|
$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
|
public function index(): string
|
||||||
{
|
{
|
||||||
$user_id = get_cookie('save_id');
|
$user_id = get_cookie('save_id');
|
||||||
@@ -59,7 +91,7 @@ class Login extends BaseController
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!$this->validate($rules)) {
|
if (!$this->validate($rules)) {
|
||||||
return $this->response->setJSON([
|
return $this->jsonResponse([
|
||||||
'code' => '1',
|
'code' => '1',
|
||||||
'errors' => $this->validator->getErrors()
|
'errors' => $this->validator->getErrors()
|
||||||
]);
|
]);
|
||||||
@@ -82,7 +114,7 @@ class Login extends BaseController
|
|||||||
|
|
||||||
$this->loginModel->insertUserLog($logs);
|
$this->loginModel->insertUserLog($logs);
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->jsonResponse([
|
||||||
'code' => '1',
|
'code' => '1',
|
||||||
'msg' => '존재하지 않는 아이디입니다.'
|
'msg' => '존재하지 않는 아이디입니다.'
|
||||||
]);
|
]);
|
||||||
@@ -96,7 +128,7 @@ class Login extends BaseController
|
|||||||
|
|
||||||
$this->loginModel->insertUserLog($logs);
|
$this->loginModel->insertUserLog($logs);
|
||||||
|
|
||||||
return $this->response->setJSON(body: [
|
return $this->jsonResponse([
|
||||||
'code' => '1',
|
'code' => '1',
|
||||||
'msg' => '잘못된 비밀번호 입니다.'
|
'msg' => '잘못된 비밀번호 입니다.'
|
||||||
]);
|
]);
|
||||||
@@ -135,7 +167,7 @@ class Login extends BaseController
|
|||||||
|
|
||||||
$this->session->set($newdata);
|
$this->session->set($newdata);
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->jsonResponse([
|
||||||
'code' => '0',
|
'code' => '0',
|
||||||
'msg' => 'success'
|
'msg' => 'success'
|
||||||
]);
|
]);
|
||||||
@@ -148,7 +180,17 @@ class Login extends BaseController
|
|||||||
log_message('error', '[LOGIN ERROR] ' . $e->getMessage());
|
log_message('error', '[LOGIN ERROR] ' . $e->getMessage());
|
||||||
log_message('error', $e->getTraceAsString());
|
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',
|
'code' => '9',
|
||||||
'msg' => '서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'
|
'msg' => '서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -13,11 +13,32 @@ class AuthCheck implements FilterInterface
|
|||||||
$session = session();
|
$session = session();
|
||||||
log_message('debug', 'URI PATH: ' . service('uri')->getPath());
|
log_message('debug', 'URI PATH: ' . service('uri')->getPath());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 세션 읽기 시도
|
||||||
|
$loggedIn = $session->get('logged_in');
|
||||||
|
|
||||||
// 로그인 체크
|
// 로그인 체크
|
||||||
if (!$session->get('logged_in')) {
|
if (!$loggedIn) {
|
||||||
// 로그인 안 되어 있으면 로그인 페이지로
|
// 로그인 안 되어 있으면 로그인 페이지로
|
||||||
return redirect()->to('/login');
|
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', '세션 서비스 오류입니다. 시스템 관리자에게 문의해주세요.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||||
|
|||||||
82
app/Helpers/redis_helper.php
Normal file
82
app/Helpers/redis_helper.php
Normal 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', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,9 +53,17 @@ class TypeSHandler
|
|||||||
|
|
||||||
// 2. Result 데이터 저장
|
// 2. Result 데이터 저장
|
||||||
$resultData = $this->parameterMapper->mapResult($rcptSq, $rawData);
|
$resultData = $this->parameterMapper->mapResult($rcptSq, $rawData);
|
||||||
|
write_custom_log("Result Insert 데이터: " . json_encode($resultData, JSON_UNESCAPED_UNICODE), 'INFO', 'service');
|
||||||
|
|
||||||
if (!$this->resultModel->insert($resultData)) {
|
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'));
|
CLI::write(CLI::color('✅ Result 저장 성공', 'blue'));
|
||||||
|
|
||||||
// 3. 트랜잭션 커밋
|
// 3. 트랜잭션 커밋
|
||||||
|
|||||||
@@ -115,21 +115,39 @@ class TypeSParameterMapper extends BaseParameterMapper
|
|||||||
$charge = null;
|
$charge = null;
|
||||||
if ($sectorNumber) {
|
if ($sectorNumber) {
|
||||||
$charge = $this->regionModel->getChargeByRegionCd($sectorNumber);
|
$charge = $this->regionModel->getChargeByRegionCd($sectorNumber);
|
||||||
|
log_message('info', "[TypeSParameterMapper] 지역코드: {$sectorNumber}, 조회 결과: " . json_encode($charge, JSON_UNESCAPED_UNICODE));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 기본 담당자 설정 (지역별 담당자가 없으면)
|
// 2. 기본 담당자 설정 (지역별 담당자가 없으면)
|
||||||
$deptSq = $charge['dept_sq'] ?? '26';
|
$deptSq = $charge['dept_sq'] ?? '26';
|
||||||
$usrSq = $charge['usr_sq'] ?? '1';
|
$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 검증인 경우 환경별 담당자로 덮어쓰기
|
// 3. VR 검증인 경우 환경별 담당자로 덮어쓰기
|
||||||
if ($isVrVerification) {
|
if ($isVrVerification) {
|
||||||
|
log_message('info', "[TypeSParameterMapper] ENVIRONMENT 상수 값: " . ENVIRONMENT);
|
||||||
|
|
||||||
if (ENVIRONMENT === 'development') {
|
if (ENVIRONMENT === 'development') {
|
||||||
$deptSq = '29';
|
$deptSq = '29';
|
||||||
$usrSq = '472';
|
$usrSq = '472'; // vradmin
|
||||||
|
log_message('info', "[TypeSParameterMapper] VR 검증 (DEV) - dept_sq: {$deptSq}, usr_sq: {$usrSq}");
|
||||||
} else {
|
} else {
|
||||||
// production
|
// production - TODO: 실제 프로덕션 VR 담당자로 변경 필요
|
||||||
$deptSq = '33';
|
$deptSq = '29';
|
||||||
$usrSq = '1993';
|
$usrSq = '472'; // 임시: vradmin (프로덕션 VR 담당자 확인 후 수정)
|
||||||
|
log_message('warning', "[TypeSParameterMapper] VR 검증 (PROD) - 프로덕션 VR 담당자 미설정, 임시 담당자 사용: usr_sq={$usrSq}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,15 @@
|
|||||||
<div class="h-100 bg-plum-plate bg-animation">
|
<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="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">
|
<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"
|
<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);">
|
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">
|
<div class="card-body p-5">
|
||||||
@@ -202,6 +211,27 @@
|
|||||||
const tpl = document.querySelector('.my-loader-template');
|
const tpl = document.querySelector('.my-loader-template');
|
||||||
var table;
|
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 () {
|
$(function () {
|
||||||
|
|
||||||
$("#btnSearch").on("click", function () {
|
$("#btnSearch").on("click", function () {
|
||||||
@@ -231,6 +261,9 @@
|
|||||||
console.log(xhr.responseText);
|
console.log(xhr.responseText);
|
||||||
},
|
},
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
|
// 시스템 상태 체크 (Redis 장애 여부)
|
||||||
|
checkSystemStatus(result);
|
||||||
|
|
||||||
if (result.code === "0") {
|
if (result.code === "0") {
|
||||||
location.href = '/'
|
location.href = '/'
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,40 @@
|
|||||||
* - 프레임워크를 로드하지 않아 매우 빠르고 안전함
|
* - 프레임워크를 로드하지 않아 매우 빠르고 안전함
|
||||||
* - 받은 데이터를 Redis 큐에 넣고 즉시 응답 테스트
|
* - 받은 데이터를 Redis 큐에 넣고 즉시 응답 테스트
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// .env 파일 로드 함수 (프레임워크 미사용 환경)
|
||||||
|
function loadEnvFile($filePath) {
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
// 주석 제거
|
||||||
|
if (strpos(trim($line), '#') === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEY = VALUE 파싱
|
||||||
|
if (strpos($line, '=') !== false) {
|
||||||
|
list($key, $value) = explode('=', $line, 2);
|
||||||
|
$key = trim($key);
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
// 따옴표 제거
|
||||||
|
$value = trim($value, '"\'');
|
||||||
|
|
||||||
|
// 환경변수 설정 (이미 있으면 덮어쓰지 않음)
|
||||||
|
if (!getenv($key)) {
|
||||||
|
putenv("$key=$value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .env 파일 로드 (한 단계 상위 디렉토리)
|
||||||
|
loadEnvFile(__DIR__ . '/../.env');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 날짜별 로그 기록 함수
|
* 날짜별 로그 기록 함수
|
||||||
* @param string $message 로그 내용
|
* @param string $message 로그 내용
|
||||||
@@ -76,52 +110,96 @@ writeLog("REQUEST_INFO | " . json_encode($requestInfo, JSON_UNESCAPED_UNICODE |
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
// 2. 보안 키 체크 (URL 파라미터 key=값)
|
// 2. 보안 키 체크 (URL 파라미터 key=값)
|
||||||
$configKey = "7EE868F4B36D36B3D86736828F4729EAC4992083"; // 실제 사용할 키값으로 변경하세요
|
$configKey = getenv('API_RECEIVER_KEY') ?: "7EE868F4B36D36B3D86736828F4729EAC4992083";
|
||||||
$receivedKey = $_GET['key'] ?? '';
|
$receivedKey = $_GET['key'] ?? '';
|
||||||
$logDir = __DIR__ . '/logs/';
|
|
||||||
|
|
||||||
if ($receivedKey !== $configKey) {
|
if ($receivedKey !== $configKey) {
|
||||||
writeLog("SECURITY_FAIL | Invalid key: $receivedKey", 'WARNING');
|
writeLog("SECURITY_FAIL | Invalid key: $receivedKey", 'WARNING');
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo apiResponse([
|
echo apiResponse([
|
||||||
'code' => '-1',
|
'code' => 'E403',
|
||||||
'message' => 'Unregistered key'
|
'message' => 'Unregistered key'
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 3. 데이터 수신 (POST JSON 또는 GET 파라미터)
|
// 3. 데이터 수신 및 검증 (POST JSON)
|
||||||
|
if (empty($rawData)) {
|
||||||
|
throw new Exception("No data received");
|
||||||
|
}
|
||||||
|
|
||||||
$data = json_decode($rawData, true);
|
$data = json_decode($rawData, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
throw new Exception("Invalid JSON format: " . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($data)) {
|
if (empty($data)) {
|
||||||
throw new Exception("Empty data received");
|
throw new Exception("Empty data received");
|
||||||
}
|
}
|
||||||
// 4. Redis 연결
|
|
||||||
$redis = new Redis();
|
|
||||||
// Docker 서비스 이름인 'redis' 사용
|
|
||||||
$redisHost = getenv('REDIS_HOST') ?: 'infra-redis'; // ✅ 가능
|
|
||||||
$redisPort = getenv('REDIS_PORT') ?: 6379;
|
|
||||||
$success = $redis->connect($redisHost, $redisPort);
|
|
||||||
|
|
||||||
if (!$success) {
|
// 4. 페이로드 준비
|
||||||
throw new Exception("Could not connect to Redis");
|
|
||||||
}
|
|
||||||
|
|
||||||
$redis->select(9); // 10번 DB 사용
|
|
||||||
|
|
||||||
// 5. 큐에 넣을 데이터 포맷팅
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'request_data' => $data,
|
'request_data' => $data,
|
||||||
'received_at' => date('Y-m-d H:i:s'),
|
'received_at' => date('Y-m-d H:i:s'),
|
||||||
'client_ip' => $_SERVER['REMOTE_ADDR']
|
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
|
||||||
];
|
];
|
||||||
|
|
||||||
// 'naver:raw_queue'라는 이름의 리스트에 저장
|
// 5. Redis 연결 시도 및 폴백 처리
|
||||||
$redis->lPush('naver:raw_queue', json_encode($payload));
|
$redisSuccess = false;
|
||||||
|
$redis = new Redis();
|
||||||
|
|
||||||
// 6. 네이버측에 성공 응답 (202 Accepted)
|
try {
|
||||||
// 처리가 완료된 것은 아니지만, 접수는 완료되었음을 의미
|
// .env 환경변수에서 Redis 설정 읽기
|
||||||
|
$redisHost = getenv('REDIS_HOST') ?: '127.0.0.1';
|
||||||
|
$redisPort = getenv('REDIS_PORT') ?: 6379;
|
||||||
|
$redisDatabase = getenv('REDIS_DATABASE') ?: 9;
|
||||||
|
|
||||||
|
$success = $redis->connect($redisHost, (int)$redisPort, 2.5); // 2.5초 타임아웃
|
||||||
|
|
||||||
|
if (!$success) {
|
||||||
|
throw new Exception("Could not connect to Redis at {$redisHost}:{$redisPort}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$redis->select((int)$redisDatabase);
|
||||||
|
|
||||||
|
// 'naver:raw_queue'라는 이름의 리스트에 저장
|
||||||
|
$pushResult = $redis->lPush('naver:raw_queue', json_encode($payload));
|
||||||
|
|
||||||
|
if (!$pushResult) {
|
||||||
|
throw new Exception("Failed to push data to Redis queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
$redisSuccess = true;
|
||||||
|
writeLog("SUCCESS | Redis queue length: {$pushResult} | IP: " . ($payload['client_ip'] ?? 'unknown'), 'INFO');
|
||||||
|
|
||||||
|
} catch (Exception $redisError) {
|
||||||
|
// Redis 실패 시 파일 폴백
|
||||||
|
writeLog("REDIS_FAIL | " . $redisError->getMessage() . " | Using file fallback", 'WARNING');
|
||||||
|
|
||||||
|
// 폴백 디렉토리 생성 및 권한 설정
|
||||||
|
$fallbackDir = __DIR__ . '/fallback_queue';
|
||||||
|
if (!is_dir($fallbackDir)) {
|
||||||
|
if (!mkdir($fallbackDir, 0777, true) && !is_dir($fallbackDir)) {
|
||||||
|
throw new Exception("Failed to create fallback directory");
|
||||||
|
}
|
||||||
|
chmod($fallbackDir, 0777);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일로 저장 (타임스탬프 + 유니크ID)
|
||||||
|
$filename = $fallbackDir . '/' . date('YmdHis') . '_' . uniqid() . '.json';
|
||||||
|
$writeResult = file_put_contents($filename, json_encode($payload), LOCK_EX);
|
||||||
|
|
||||||
|
if ($writeResult === false) {
|
||||||
|
throw new Exception("Failed to write fallback file");
|
||||||
|
}
|
||||||
|
|
||||||
|
chmod($filename, 0666);
|
||||||
|
writeLog("FALLBACK_SUCCESS | File: " . basename($filename) . " | IP: " . ($payload['client_ip'] ?? 'unknown'), 'INFO');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 성공 응답 (Redis 또는 Fallback 성공)
|
||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
echo apiResponse([
|
echo apiResponse([
|
||||||
'code' => 'success',
|
'code' => 'success',
|
||||||
@@ -130,13 +208,13 @@ try {
|
|||||||
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// 7. 장애 발생 시 로그 기록 (시스템 로그)
|
// 7. 완전 장애 발생 시 (Redis도 실패, File도 실패)
|
||||||
writeLog( 'Exception :' . apiResponse($data) , 'ERROR');
|
writeLog('CRITICAL_ERROR: ' . $e->getMessage() . ' | Data: ' . substr($rawData, 0, 200), 'ERROR');
|
||||||
|
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|
||||||
echo apiResponse([
|
echo apiResponse([
|
||||||
'code' => '-1',
|
'code' => 'E999',
|
||||||
'message' => $e->getMessage()
|
'message' => 'Internal server error'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user