- layouts/app.blade.php에 SweetAlert2 CDN 및 전역 헬퍼 함수 추가 - showToast(): 토스트 알림 (success, error, warning, info) - showConfirm(): 확인 대화상자 - showDeleteConfirm(): 삭제 확인 (경고 아이콘) - showPermanentDeleteConfirm(): 영구 삭제 확인 (빨간색 경고) - showSuccess(), showError(): 성공/에러 알림 - 변환된 파일 목록 (48개 Blade 파일): - menus/* (6개), boards/* (2개), posts/* (3개) - daily-logs/* (3개), project-management/* (6개) - dev-tools/flow-tester/* (6개) - quote-formulas/* (4개), permission-analyze/* (1개) - archived-records/* (1개), profile/* (1개) - roles/* (3개), permissions/* (3개) - departments/* (3개), tenants/* (3개), users/* (3개) - 주요 개선사항: - Tailwind CSS 테마와 일관된 디자인 - 비동기 콜백 패턴으로 리팩토링 - 삭제/복원/영구삭제 각각 다른 스타일 적용
196 lines
5.9 KiB
PHP
196 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\FlowTester;
|
|
|
|
/**
|
|
* API 로그 캡처 서비스
|
|
*
|
|
* API 서버의 laravel.log에서 Request/Response 로그를 캡처합니다.
|
|
* 플로우 실행 시작/종료 시점의 파일 오프셋을 이용하여
|
|
* 해당 실행 중에 발생한 로그만 추출합니다.
|
|
*/
|
|
class ApiLogCapturer
|
|
{
|
|
private string $logPath;
|
|
|
|
private int $startOffset = 0;
|
|
|
|
public function __construct()
|
|
{
|
|
// API 로그 파일 경로 (Docker/로컬 환경 모두 지원)
|
|
$this->logPath = $this->resolveLogPath();
|
|
}
|
|
|
|
/**
|
|
* 캡처 시작 (현재 로그 파일 위치 기록)
|
|
*/
|
|
public function start(): void
|
|
{
|
|
if (file_exists($this->logPath)) {
|
|
$this->startOffset = filesize($this->logPath);
|
|
} else {
|
|
$this->startOffset = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 캡처 종료 및 로그 추출
|
|
*
|
|
* @return array 파싱된 API 로그 배열
|
|
*/
|
|
public function capture(): array
|
|
{
|
|
if (! file_exists($this->logPath)) {
|
|
return [];
|
|
}
|
|
|
|
$currentSize = filesize($this->logPath);
|
|
|
|
// 새로운 로그가 없으면 빈 배열 반환
|
|
if ($currentSize <= $this->startOffset) {
|
|
return [];
|
|
}
|
|
|
|
// 새로 추가된 로그 읽기
|
|
$handle = fopen($this->logPath, 'r');
|
|
if (! $handle) {
|
|
return [];
|
|
}
|
|
|
|
fseek($handle, $this->startOffset);
|
|
$newLogs = fread($handle, $currentSize - $this->startOffset);
|
|
fclose($handle);
|
|
|
|
// Request/Response 로그만 파싱
|
|
return $this->parseLogs($newLogs);
|
|
}
|
|
|
|
/**
|
|
* API 로그 파일 경로 결정
|
|
*/
|
|
private function resolveLogPath(): string
|
|
{
|
|
// 1. 환경 변수로 지정된 경로
|
|
$envPath = env('API_LOG_PATH');
|
|
if ($envPath && file_exists($envPath)) {
|
|
return $envPath;
|
|
}
|
|
|
|
// 2. 로컬 개발 환경 (상대 경로)
|
|
$localPath = base_path('../api/storage/logs/laravel.log');
|
|
if (file_exists($localPath)) {
|
|
return $localPath;
|
|
}
|
|
|
|
// 3. Docker 환경 (절대 경로)
|
|
$dockerPath = '/var/www/api/storage/logs/laravel.log';
|
|
if (file_exists($dockerPath)) {
|
|
return $dockerPath;
|
|
}
|
|
|
|
// 4. 기본값 (로컬 경로)
|
|
return $localPath;
|
|
}
|
|
|
|
/**
|
|
* 로그 텍스트 파싱
|
|
*
|
|
* Laravel 로그 형식:
|
|
* [2025-12-04 12:30:53] local.INFO: API Request {...}
|
|
* [2025-12-04 12:30:53] local.INFO: API Response {...}
|
|
*/
|
|
private function parseLogs(string $rawLogs): array
|
|
{
|
|
$logs = [];
|
|
// 전체 로그 라인 매칭 (원본 보존용)
|
|
$pattern = '/(\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \w+\.\w+: (API Request|API Response) (\{.+?\}))(?=\s*\[\d{4}|\s*$)/s';
|
|
|
|
if (preg_match_all($pattern, $rawLogs, $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $match) {
|
|
$rawLine = $match[1]; // 전체 원본 로그 라인
|
|
$timestamp = $match[2];
|
|
$type = $match[3] === 'API Request' ? 'request' : 'response';
|
|
$jsonStr = $match[4];
|
|
|
|
// JSON 파싱
|
|
$data = json_decode($jsonStr, true);
|
|
if ($data === null) {
|
|
continue;
|
|
}
|
|
|
|
$log = [
|
|
'timestamp' => $timestamp,
|
|
'type' => $type,
|
|
'raw' => $this->decodeUnicodeEscapes($rawLine), // 유니코드 이스케이프 디코딩
|
|
];
|
|
|
|
if ($type === 'request') {
|
|
$log['method'] = $data['method'] ?? '';
|
|
$log['uri'] = $data['uri'] ?? '';
|
|
$log['input'] = $data['input'] ?? [];
|
|
$log['ip'] = $data['ip'] ?? '';
|
|
} else {
|
|
$log['uri'] = $data['uri'] ?? '';
|
|
$log['status'] = $data['status'] ?? 0;
|
|
|
|
// content는 JSON 문자열이므로 파싱
|
|
$content = $data['content'] ?? '';
|
|
if (is_string($content)) {
|
|
$parsedContent = json_decode($content, true);
|
|
if ($parsedContent !== null) {
|
|
$log['success'] = $parsedContent['success'] ?? null;
|
|
$log['message'] = $parsedContent['message'] ?? '';
|
|
|
|
// 에러 정보가 있으면 포함
|
|
if (isset($parsedContent['error'])) {
|
|
$log['error'] = $parsedContent['error'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$logs[] = $log;
|
|
}
|
|
}
|
|
|
|
return $logs;
|
|
}
|
|
|
|
/**
|
|
* 로그 파일 경로 반환 (디버깅용)
|
|
*/
|
|
public function getLogPath(): string
|
|
{
|
|
return $this->logPath;
|
|
}
|
|
|
|
/**
|
|
* 로그 파일 존재 여부 확인
|
|
*/
|
|
public function isAvailable(): bool
|
|
{
|
|
return file_exists($this->logPath) && is_readable($this->logPath);
|
|
}
|
|
|
|
/**
|
|
* 유니코드 이스케이프 시퀀스 디코딩
|
|
*
|
|
* JSON 내부의 이중 이스케이프된 유니코드를 실제 문자로 변환
|
|
* 예: \\u0000 또는 \\\\u0000 → 실제 유니코드 문자
|
|
*/
|
|
private function decodeUnicodeEscapes(string $str): string
|
|
{
|
|
// 이중 이스케이프 (\\\\uXXXX) 디코딩
|
|
$str = preg_replace_callback('/\\\\\\\\u([0-9a-fA-F]{4})/', function ($match) {
|
|
return json_decode('"\u'.$match[1].'"') ?? $match[0];
|
|
}, $str);
|
|
|
|
// 단일 이스케이프 (\\uXXXX) 디코딩
|
|
$str = preg_replace_callback('/\\\\u([0-9a-fA-F]{4})/', function ($match) {
|
|
return json_decode('"\u'.$match[1].'"') ?? $match[0];
|
|
}, $str);
|
|
|
|
return $str;
|
|
}
|
|
}
|