계획서 §검증 후반부를 다시 대조해 미구현 3건을 확인하고 모두 구현했다. 앞선 커밋에서 "미구현 1건" 으로 보고한 것은 §12 불변식 표만 파일로 대조하고 §검증 섹션의 표 두 개는 확인하지 않은 결과였다. - 설치 2단계 요구사항 화면에 자산 URL 방식 항목 추가. 판정은 서버가 아니라 브라우저가 한다 — 서버에서 자기 APP_URL 로 요청하면 loopback 이 vhost·프록시 체인을 우회해 실제 방문자와 다른 답을 낸다. 2단계와 3단계가 같은 판정 함수를 쓰도록 추출했다 - 대시보드가 스스로 프로브를 던져 **저장된** 방식과 대조하고 어긋나면 안내한다. 런타임 값이 아니라 저장값을 보는 이유는, 자가 복구가 이미 전환해 놓았을 때가 바로 알려야 할 상황이기 때문이다 — 봇은 자바스크립트를 실행하지 않아 자가 복구가 닿지 않는다. 알리기만 하고 저장하지 않으므로 클라이언트가 서버 설정을 뒤집지 않는다 - 레이아웃 렌더링 테스트를 두 모드로 추가. 핵심 단언은 두 모드의 DOM 이 완전히 동일하다는 것이다 — 모드가 화면 구조로 새어나가면 확장자 없는 환경에서만 깨지고 그 환경은 개발 중 거의 재현되지 않는다 함께 고친 테스트 격리 결함 4건 (이번 기능과 무관하나 같은 세션에서 발견): - 인스톨러 공유 클래스 로드가 오토로드를 막고 있어, 앱 루트를 임시 디렉토리로 바꾸는 테스트에서 없는 파일을 읽으려다 죽었다 (8건 fatal) - 테스트용 번역 스텁이 실제 구현과의 정의 경쟁에서 이기면 주입된 번역을 무시했다. 클래스 단독 실행은 통과하고 스위트 실행만 실패하던 원인 - 모듈 활성 목록은 "디스크 로드분 ∩ DB 활성분" 인데 부팅 시 테이블이 비어 디스크 스캔이 일어나지 않아, 파일 캐시 잔재가 있을 때만 통과하던 순서 의존 테스트 - 라우트 프로바이더 가드 테스트가 Git 미추적 파일(.env) 존재에 의존해 신규 클론·CI 에서 실패했다. 임시 앱 루트로 격리 또한 일부 테스트가 실제 개발 환경 파일을 삭제·재생성하고 자신의 정리 단계에서 되돌리는데, 그 테스트가 죽으면 되돌아오지 않아 복구 불가능한 유실이 발생한다. 실제로 이번 세션에서 겪었으므로 테스트 부트스트랩에 프로세스 수준 복원 안전망을 추가했다. 검증: 백엔드 344건 / 템플릿 2799건 / 레이아웃 7건 / 브라우저 9건 green, audit 위반 0.
231 lines
8.1 KiB
PHP
231 lines
8.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
* 그누보드7 웹 인스톨러 설정 파일
|
|
*
|
|
* 인스톨러의 기본 상수와 설정을 정의합니다.
|
|
*
|
|
* @package G7\Installer
|
|
*/
|
|
|
|
// 프로젝트 루트 경로 (public/install/includes에서 3단계 상위)
|
|
if (!defined('BASE_PATH')) {
|
|
define('BASE_PATH', realpath(dirname(__DIR__, 3)) ?: dirname(__DIR__, 3));
|
|
}
|
|
|
|
// 인스톨러 기본 URL
|
|
$installerPath = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
|
|
|
|
// install이 없으면 추가
|
|
if (substr($installerPath, -8) !== '/install') {
|
|
$installerPath .= '/install';
|
|
}
|
|
|
|
// 다음 define 들은 가드 필수 — 테스트 환경에서 같은 프로세스가 config.php 를 직접/간접
|
|
// 재진입(예: Laravel bootstrap + 테스트 require)할 수 있어 PHP 9 에서 fatal 이 되는
|
|
// 중복 정의 warning 을 차단.
|
|
if (!defined('INSTALLER_BASE_URL')) {
|
|
define('INSTALLER_BASE_URL', $installerPath);
|
|
}
|
|
|
|
// 소프트웨어 최초 출시 연도 (저작권 표시용)
|
|
if (!defined('APP_RELEASE_YEAR')) {
|
|
define('APP_RELEASE_YEAR', '2026');
|
|
}
|
|
|
|
// 최소 PHP 버전
|
|
if (!defined('MIN_PHP_VERSION')) {
|
|
define('MIN_PHP_VERSION', '8.2.0');
|
|
}
|
|
|
|
// 필수 PHP 모듈
|
|
if (!defined('REQUIRED_EXTENSIONS')) {
|
|
define('REQUIRED_EXTENSIONS', [
|
|
'pdo',
|
|
'mbstring',
|
|
'openssl',
|
|
'tokenizer',
|
|
'xml',
|
|
'ctype',
|
|
'json',
|
|
'fileinfo',
|
|
'curl',
|
|
'dom',
|
|
'filter',
|
|
'hash',
|
|
'pcre',
|
|
'session',
|
|
]);
|
|
}
|
|
|
|
// 선택적 PHP 모듈 (설치를 권장하지만 필수는 아님)
|
|
if (!defined('OPTIONAL_EXTENSIONS')) {
|
|
define('OPTIONAL_EXTENSIONS', [
|
|
'zlib', // gzip 압축 지원 (응답 압축에 사용)
|
|
'gd', // 이미지 처리
|
|
'imagick', // 고급 이미지 처리
|
|
'redis', // Redis 캐시 지원
|
|
'intl', // 국제화 지원
|
|
]);
|
|
}
|
|
|
|
// 최소 디스크 공간 (MB)
|
|
if (!defined('MIN_DISK_SPACE_MB')) {
|
|
define('MIN_DISK_SPACE_MB', 500);
|
|
}
|
|
|
|
// DB 테이블 접두사 최대 길이.
|
|
// MySQL identifier 한도(64자) 안에서 자동 생성 인덱스명이 안전하도록 제한한다.
|
|
// 가장 긴 자동 생성 인덱스명(접두사 제외)이 58자이므로 58 + 6 = 64 로 정확히 한도에 맞는다.
|
|
// 7자 이상 접두사는 일부 인덱스명이 65자가 되어 마이그레이션이 실패한다.
|
|
if (!defined('MAX_DB_PREFIX_LENGTH')) {
|
|
define('MAX_DB_PREFIX_LENGTH', 6);
|
|
}
|
|
|
|
// 디렉토리 권한 설정 (8진수)
|
|
// 업계 표준 755 (WordPress/Drupal/Joomla/Laravel 공통) — 실제 통과 기준은 is_writable() && is_readable()
|
|
if (!defined('REQUIRED_DIRECTORY_PERMISSIONS')) {
|
|
define('REQUIRED_DIRECTORY_PERMISSIONS', 0755);
|
|
}
|
|
|
|
// 권한 표시용 문자열 (사용자에게 보여줄 형식)
|
|
if (!defined('REQUIRED_DIRECTORY_PERMISSIONS_DISPLAY')) {
|
|
define('REQUIRED_DIRECTORY_PERMISSIONS_DISPLAY', '755');
|
|
}
|
|
|
|
// 권한 검증이 필요한 디렉토리 목록
|
|
// 값이 true인 경우 하위 디렉토리까지 재귀적으로 체크
|
|
if (!defined('REQUIRED_DIRECTORIES')) {
|
|
define('REQUIRED_DIRECTORIES', [
|
|
'storage' => true,
|
|
'bootstrap/cache' => false,
|
|
'vendor' => false,
|
|
'modules' => false,
|
|
'modules/_pending' => false,
|
|
'plugins' => false,
|
|
'plugins/_pending' => false,
|
|
'templates' => false,
|
|
'templates/_pending' => false,
|
|
'lang-packs' => false, // 언어팩 활성 디렉토리 (코어/모듈/플러그인/템플릿 lang-packs)
|
|
'lang-packs/_pending' => false, // 외부 ZIP/URL/GitHub 다운로드 staging
|
|
'storage/app/core_pending' => false, // 코어 업데이트 _pending (기본값, 커스텀 경로는 Step 3에서 설정)
|
|
]);
|
|
}
|
|
|
|
// 인스톨러 기본 설정값
|
|
if (!defined('DEFAULT_INSTALL_CONFIG')) {
|
|
define('DEFAULT_INSTALL_CONFIG', [
|
|
// Write DB 설정
|
|
'db_write_host' => 'localhost',
|
|
'db_write_port' => '3306',
|
|
'db_write_database' => '',
|
|
'db_write_username' => '',
|
|
'db_write_password' => '',
|
|
'db_prefix' => 'g7_',
|
|
|
|
// Read DB 설정
|
|
'use_read_db' => false,
|
|
'db_read_host' => 'localhost',
|
|
'db_read_port' => '3306',
|
|
'db_read_database' => '',
|
|
'db_read_username' => '',
|
|
'db_read_password' => '',
|
|
|
|
// 사이트 설정
|
|
'app_name' => '그누보드7',
|
|
'app_env' => 'production',
|
|
|
|
// 관리자 계정 설정
|
|
'admin_name' => '관리자',
|
|
'admin_email' => '',
|
|
'admin_password' => '',
|
|
'admin_language' => 'ko',
|
|
|
|
// 코어 업데이트 설정
|
|
'core_update_pending_path' => '', // 빈 값 = 기본값(storage/app/core_pending) 사용
|
|
'core_update_github_url' => 'https://github.com/gnuboard/g7',
|
|
'core_update_github_token' => '', // GitHub Personal Access Token (프라이빗 저장소용)
|
|
|
|
// PHP CLI / Composer 경로 설정
|
|
'php_binary' => 'php', // PHP CLI 바이너리 경로 (예: /usr/local/php82/bin/php)
|
|
'composer_binary' => '', // Composer 바이너리 경로 (빈 값 = 시스템 PATH 사용)
|
|
|
|
// Vendor 설치 모드 (auto|composer|bundled)
|
|
// - auto: composer 사용 가능 시 composer, 불가 시 vendor-bundle.zip 추출
|
|
// - composer: 강제 composer 실행
|
|
// - bundled: 강제 vendor-bundle.zip 추출 (공유 호스팅 환경)
|
|
'vendor_mode' => 'auto',
|
|
]);
|
|
}
|
|
|
|
// 설치 단계별 파일 매핑
|
|
// Step 5 (installation)에서 완료/실패/중단 화면까지 모두 처리
|
|
if (!defined('STEP_FILE_MAP')) {
|
|
define('STEP_FILE_MAP', [
|
|
0 => 'welcome',
|
|
1 => 'license',
|
|
2 => 'requirements',
|
|
3 => 'configuration',
|
|
4 => 'extension-selection',
|
|
5 => 'installation',
|
|
]);
|
|
}
|
|
|
|
// 인스톨러 기본 상태 정의
|
|
if (!defined('DEFAULT_INSTALLATION_STATE')) {
|
|
define('DEFAULT_INSTALLATION_STATE', [
|
|
'current_step' => 0,
|
|
'step_status' => [
|
|
'0' => 'pending',
|
|
'1' => 'pending',
|
|
'2' => 'pending',
|
|
'3' => 'pending',
|
|
'4' => 'pending',
|
|
'5' => 'pending',
|
|
],
|
|
'completed_tasks' => [],
|
|
'current_task' => null,
|
|
'current_task_name' => null,
|
|
'installation_status' => 'not_started',
|
|
'config' => [],
|
|
'selected_extensions' => [
|
|
'admin_templates' => [],
|
|
'user_templates' => [],
|
|
'modules' => [],
|
|
'plugins' => [],
|
|
'language_packs' => [],
|
|
],
|
|
'extension_names' => [], // 확장 이름 매핑 (identifier → {ko: '...', en: '...'})
|
|
]);
|
|
}
|
|
|
|
// 지원 언어 목록 (언어 추가 시 이 한 곳만 수정)
|
|
if (!defined('SUPPORTED_LANGUAGES')) {
|
|
define('SUPPORTED_LANGUAGES', [
|
|
'ko' => '한국어 (Korean)',
|
|
'en' => 'English',
|
|
]);
|
|
}
|
|
|
|
// 설치 완료 후 인스톨러 파일 삭제 여부
|
|
if (!defined('DELETE_INSTALLER_AFTER_COMPLETE')) {
|
|
define('DELETE_INSTALLER_AFTER_COMPLETE', true);
|
|
}
|
|
|
|
// 코어와 공유하는 판정 SSoT 클래스 로드.
|
|
//
|
|
// 인스톨러는 Laravel 오토로드 없이 도는 순수 PHP 이므로 파일을 직접 require 한다.
|
|
// 두 클래스는 프레임워크 의존성이 0 이라 이 방식이 성립하며, 인스톨러와 코어가
|
|
// 서로 다른 판정을 내리는 것을 구조적으로 차단한다.
|
|
// class_exists 가드: Laravel 부트가 선행된 테스트 환경에서 중복 로드 방지.
|
|
//
|
|
// 오토로드를 **막지 않는다**(2번째 인자 생략). `false` 를 주면 오토로드 가능한
|
|
// 클래스를 못 본 채 require 로 내려가는데, BASE_PATH 를 임시 디렉토리로 바꿔 두는
|
|
// 인스톨러 단위 테스트에서는 그 경로에 app/Support 가 없어 fatal 이 된다.
|
|
// 순수 인스톨러 실행 시에는 등록된 오토로더가 없어 false 를 반환하므로 require 가 그대로 돈다.
|
|
if (!class_exists('App\\Support\\PrivilegedDatabaseAccounts')) {
|
|
require_once BASE_PATH . '/app/Support/PrivilegedDatabaseAccounts.php';
|
|
}
|
|
if (!class_exists('App\\Support\\OpcacheStatus')) {
|
|
require_once BASE_PATH . '/app/Support/OpcacheStatus.php';
|
|
} |