= 5) { if (isset($state['step_status']['5']) && $state['step_status']['5'] === 'completed') { return true; } } // g7_installed 파일 존재 여부 확인 (추가 안전장치) $installedFlagPath = BASE_PATH . '/storage/app/g7_installed'; if (file_exists($installedFlagPath)) { return true; } // .env 파일의 INSTALLER_COMPLETED 플래그 확인 $envPath = BASE_PATH . '/.env'; if (file_exists($envPath)) { $envContent = file_get_contents($envPath); if (strpos($envContent, 'INSTALLER_COMPLETED=true') !== false) { return true; } } return false; } /** * 특정 작업을 완료로 표시 * * @param string $task 작업 식별자 * @return bool 업데이트 성공 여부 */ function markTaskCompleted(string $task): bool { $state = getInstallationState(); // completed_tasks 배열에 추가 (중복 방지) if (!in_array($task, $state['completed_tasks'])) { $state['completed_tasks'][] = $task; } // 현재 작업이 완료된 작업과 같으면 초기화 if ($state['current_task'] === $task) { $state['current_task'] = null; $state['current_task_name'] = null; } return saveInstallationState($state); } /** * 특정 작업을 완료 목록에서 제거 * * @param string $task 작업 식별자 * @return bool 업데이트 성공 여부 */ function removeTaskCompleted(string $task): bool { $state = getInstallationState(); $state['completed_tasks'] = array_values( array_filter($state['completed_tasks'], fn($t) => $t !== $task) ); return saveInstallationState($state); } /** * 현재 진행 중인 작업 업데이트 * * @param string $task 작업 식별자 * @return bool 업데이트 성공 여부 */ function updateCurrentTask(string $task): bool { $state = getInstallationState(); $state['current_task'] = $task; // current_task_name은 저장하지 않음 (프론트엔드에서 task ID로 번역) return saveInstallationState($state); } /** * 설치 로그 추가 (별도 파일에 기록) * * 페이지 새로고침/재시작 시 로그가 즉시 표시되도록 * fflush() + clearstatcache()를 적용합니다. * * @param string $message 로그 메시지 * @return bool 저장 성공 여부 */ function addLog(string $message): bool { $logDir = BASE_PATH . '/storage/logs'; $logFile = $logDir . '/installation.log'; // 로그 디렉토리 확인 및 생성 시도 if (!is_dir($logDir)) { $created = @mkdir($logDir, 0775, true); if (!$created) { error_log("Failed to create log directory: {$logDir} (storage 권한 확인 필요)"); return false; } } // 로그 디렉토리 쓰기 권한 확인 if (!is_writable($logDir)) { error_log("Log directory is not writable: {$logDir}"); return false; } // Windows에서 CP949 인코딩된 메시지를 UTF-8로 변환 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $encoding = mb_detect_encoding($message, ['UTF-8', 'EUC-KR', 'CP949'], true); if ($encoding && $encoding !== 'UTF-8') { $message = mb_convert_encoding($message, 'UTF-8', $encoding); } } // 타임스탬프와 함께 로그 작성 $timestamp = date('Y-m-d H:i:s'); $logEntry = "[{$timestamp}] {$message}\n"; // 파일 캐시 초기화 (최신 상태 확인) clearstatcache(true, $logFile); // 파일이 없거나 빈 파일이면 UTF-8 BOM 추가 (Windows 텍스트 편집기 호환성) $isNewFile = !file_exists($logFile) || filesize($logFile) === 0; if ($isNewFile) { $utf8Bom = "\xEF\xBB\xBF"; $logEntry = $utf8Bom . $logEntry; } // 파일 핸들 열기 (append 모드) $handle = @fopen($logFile, 'a'); if ($handle === false) { error_log("Failed to open log file: {$logFile}"); return false; } // 배타적 잠금 flock($handle, LOCK_EX); // 쓰기 $result = fwrite($handle, $logEntry); // 버퍼 플러시 (즉시 디스크에 기록) fflush($handle); // 잠금 해제 및 닫기 flock($handle, LOCK_UN); fclose($handle); // Windows 파일 캐시 초기화 (최신 데이터 읽기 보장) clearstatcache(true, $logFile); if ($result === false) { error_log("Failed to write log file: {$logFile}"); return false; } return true; } /** * 설치 로그 조회 (파일에서 읽기) * * 페이지 새로고침 시 최신 로그를 즉시 표시하기 위해 * clearstatcache()를 적용합니다. * * @param int $offset 건너뛸 로그 줄 수 (폴링 모드 증분 조회용, 기본 0) * @return array 로그 배열 [{timestamp, message}, ...] */ function getInstallationLogs(int $offset = 0): array { $logFile = BASE_PATH . '/storage/logs/installation.log'; clearstatcache(true, $logFile); if (!file_exists($logFile)) { return []; } $content = @file_get_contents($logFile); if ($content === false) { return []; } $lines = explode("\n", trim($content)); $logs = []; foreach ($lines as $line) { if (empty($line)) { continue; } if (preg_match('/^\[(.+?)\] (.+)$/', $line, $matches)) { $logs[] = [ 'timestamp' => $matches[1], 'message' => $matches[2], ]; } else { $logs[] = [ 'timestamp' => date('Y-m-d H:i:s'), 'message' => $line, ]; } } // 증분 조회 (offset > 0이면 이전 결과 이후만 반환) if ($offset > 0 && $offset < count($logs)) { return array_slice($logs, $offset); } elseif ($offset >= count($logs)) { return []; } return $logs; } /** * 설치 로그 전체 줄 수 조회 (폴링 모드 offset 계산용). * * @return int 전체 로그 줄 수 */ function getInstallationLogCount(): int { $logFile = BASE_PATH . '/storage/logs/installation.log'; clearstatcache(true, $logFile); if (!file_exists($logFile)) { return 0; } $content = @file_get_contents($logFile); if ($content === false) { return 0; } $lines = explode("\n", trim($content)); $count = 0; foreach ($lines as $line) { if (!empty($line)) { $count++; } } return $count; } /** * 마지막 완료된 단계 조회 * * @return int 마지막 완료된 단계 번호 (0-5) */ function getLastCompletedStep(): int { $state = getInstallationState(); // step_status를 역순으로 확인 for ($step = 5; $step >= 0; $step--) { $stepKey = (string)$step; if (isset($state['step_status'][$stepKey]) && $state['step_status'][$stepKey] === 'completed') { return $step; } } // 완료된 단계가 없으면 -1 반환 return -1; }