fix(core): 신규 설치 후 대시보드 정적 게시 실패 알림 — 마법사 권한 검사 누락·알림 이중 등록 수정
- 설치 마법사 디렉토리 권한 검사 목록(REQUIRED_DIRECTORIES)에 public/build 추가 (웹 계정이 쓸 수 없어도 막지 않고 진행해 설치 마지막 단계의 정적 게시가 실패했다) - AppServiceProvider 의 대시보드 알림 리스너 수동 등록 제거 — CoreServiceProvider 자동 발견(훅 캐시/스캔)과 이중 등록되어 같은 알림 카드가 두 장 표시됐다 - 회귀 테스트: RequiredDirectoriesPublishRootTest, StaticPublishAlertTest 중복 등록 단언 - INSTALL.md 마법사 안내 문장, CHANGELOG 7.0.10 Fixed 2건
This commit is contained in:
@@ -52,6 +52,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- 설치 마법사의 디렉토리 권한 검사 단계(`storage`·`bootstrap/cache` 와 같은 단계)에서 `public/build` 가 검사 목록에 빠져 있던 문제를 수정했습니다. 웹 서버 계정이 그 폴더에 쓸 수 없어도 마법사가 막거나 실행할 명령을 보여 주지 않은 채 설치를 진행했고, 설치 마지막 단계의 초기 화면 파일 생성이 실패한 뒤 대시보드에 「초기 화면 파일 생성 실패」 알림이 떴습니다. 이제 다른 폴더와 같은 방식으로 통과할 때까지 진행을 막고 권한 부여 명령을 안내합니다.
|
||||
- 관리자 대시보드의 시스템 알림(초기 화면 파일 생성 실패·확장 호환성·신뢰 프록시)이 같은 내용으로 두 장씩 표시되던 문제를 수정했습니다. 알림 리스너가 자동 발견과 별도 등록으로 두 번 등록된 것이 원인입니다.
|
||||
- 새로 설치할 때 일부 확장의 설치가 "Class not found" 오류로 중단되던 문제를 수정했습니다. 확장의 설정 기본값을 읽는 단계가 그 확장의 클래스 경로 등록보다 먼저 실행된 것이 원인이며, 이제 설치는 확장 파일을 복사한 직후 클래스 경로를 먼저 등록합니다. 이미 설치된 사이트의 업데이트에는 영향이 없습니다.
|
||||
- 설정 캐시가 만들어져 있는 사이트(설치 마법사·환경설정 저장·확장 업데이트를 한 번이라도 거친 대부분의 사이트)에서 코어 업데이트가 업그레이드 스텝 단계에서 "수동 재개" 안내와 함께 멈추던 문제를 수정했습니다. 새 버전으로 실행되는 스텝 프로세스가 이전 버전의 설정 캐시를 읽어 자기 자신을 옛 버전으로 오판한 것이 원인이며, 실행할 스텝이 하나도 없는 버전으로 올릴 때도 같은 안내가 나왔습니다. 같은 원인으로 새 버전이 추가한 쓰기 폴더가 `sudo` 업데이트의 권한 정리에서 빠지던 문제도 함께 바로잡았고, 이제 업데이트는 스텝 실행 전에 이전 설정 캐시를 비웁니다.
|
||||
- 명령줄로 업그레이드 스텝을 직접 재실행하거나 관리자 「시스템 최적화」를 실행한 뒤, 그 실행이 예약한 초기 화면 정적 파일 재생성이 조용히 건너뛰어지던 문제를 수정했습니다.
|
||||
|
||||
+1
-1
@@ -348,7 +348,7 @@ http://도메인/install
|
||||
| 4. 확장 선택 | 템플릿/모듈/플러그인 선택 (의존성 자동 해결) |
|
||||
| 5. 설치 실행 | "설치 시작" 버튼 클릭 후 진행 상황 모니터링 |
|
||||
|
||||
`.env` 파일 생성과 `storage` / `bootstrap/cache` 쓰기 권한 부여는 인스톨러가 환경(소유자 일치 여부 등)에 맞춰 Step 0 및 이후 단계에서 자동 안내하므로, 사전 SSH 작업으로 수행할 필요 없습니다.
|
||||
`.env` 파일 생성과 `storage` / `bootstrap/cache` / `public/build` 쓰기 권한 부여는 인스톨러가 환경(소유자 일치 여부 등)에 맞춰 Step 0 및 이후 단계에서 자동 안내하므로, 사전 SSH 작업으로 수행할 필요 없습니다.
|
||||
|
||||
**Step 3 — Vendor 설치 방식 선택:**
|
||||
|
||||
|
||||
@@ -11,9 +11,6 @@ use App\Extension\ModuleManager;
|
||||
use App\Extension\PluginManager;
|
||||
use App\Http\View\Composers\TemplateComposer;
|
||||
use App\Http\View\Composers\UserTemplateComposer;
|
||||
use App\Listeners\ExtensionCompatibilityAlertListener;
|
||||
use App\Listeners\StaticPublishFailureAlertListener;
|
||||
use App\Listeners\TrustedProxyAlertListener;
|
||||
use App\Notifications\NotificationChannelManager;
|
||||
use App\Services\ChannelReadinessService;
|
||||
use App\Services\GeoIpService;
|
||||
@@ -101,9 +98,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
View::composer('admin', TemplateComposer::class);
|
||||
View::composer('app', UserTemplateComposer::class);
|
||||
|
||||
// 코어 훅 리스너 등록 (대시보드 알림 등)
|
||||
$this->registerCoreHookListeners();
|
||||
|
||||
// SQL 쿼리 로그 설정
|
||||
$this->configureSqlQueryLogging();
|
||||
|
||||
@@ -136,43 +130,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 코어가 소유한 훅 리스너를 등록합니다.
|
||||
*
|
||||
* 확장 리스너와 달리 코어 리스너는 자동 발견 대상이 아니므로 여기서 등록한다. 목록만
|
||||
* 늘리면 되도록 루프로 둔다 — 리스너마다 등록 메서드를 복제하면 한 곳만 빠져도 그
|
||||
* 리스너가 조용히 동작하지 않는다(등록 실패는 예외도 로그도 남기지 않는다).
|
||||
*
|
||||
* 등록 대상:
|
||||
* - `ExtensionCompatibilityAlertListener` — 코어 호환성으로 자동 비활성화/재호환된 확장
|
||||
* - `StaticPublishFailureAlertListener` — 부트스트랩 리소스 정적 게시 실패 (#122)
|
||||
* - `TrustedProxyAlertListener` — 프록시 헤더 수신 중 신뢰 프록시 미설정 (#124)
|
||||
*/
|
||||
private function registerCoreHookListeners(): void
|
||||
{
|
||||
$listenerClasses = [
|
||||
ExtensionCompatibilityAlertListener::class,
|
||||
StaticPublishFailureAlertListener::class,
|
||||
TrustedProxyAlertListener::class,
|
||||
];
|
||||
|
||||
foreach ($listenerClasses as $listenerClass) {
|
||||
$listener = new $listenerClass;
|
||||
|
||||
foreach ($listenerClass::getSubscribedHooks() as $hookName => $config) {
|
||||
$method = $config['method'] ?? 'handle';
|
||||
$priority = $config['priority'] ?? 10;
|
||||
$type = $config['type'] ?? 'action';
|
||||
|
||||
if ($type === 'filter') {
|
||||
HookManager::addFilter($hookName, [$listener, $method], $priority);
|
||||
} else {
|
||||
HookManager::addAction($hookName, [$listener, $method], $priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 아웃바운드 HTTP 프록시를 설정합니다.
|
||||
*
|
||||
|
||||
@@ -107,6 +107,7 @@ if (! defined('REQUIRED_DIRECTORIES')) {
|
||||
'plugins/_pending' => false,
|
||||
'templates' => false,
|
||||
'templates/_pending' => false,
|
||||
'public/build' => false, // 초기 화면 정적 파일 게시 루트(public/build/ext)의 상위 — 설치 마지막 단계와 웹 렌더 자가 치유가 웹 계정으로 쓴다
|
||||
'lang-packs' => false, // 언어팩 활성 디렉토리 (코어/모듈/플러그인/템플릿 lang-packs)
|
||||
'lang-packs/_pending' => false, // 외부 ZIP/URL/GitHub 다운로드 staging
|
||||
'storage/app/core_pending' => false, // 코어 업데이트 _pending (기본값, 커스텀 경로는 Step 3에서 설정)
|
||||
|
||||
@@ -197,6 +197,26 @@ class StaticPublishAlertTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너는 훅에 한 번만 등록된다.
|
||||
*
|
||||
* 코어 리스너는 CoreServiceProvider 의 자동 발견(훅 캐시 또는 app/Listeners 스캔)이 등록한다.
|
||||
* AppServiceProvider 가 같은 리스너를 손으로 한 번 더 등록하면 같은 알림 카드가 두 장 뜬다 —
|
||||
* 예외도 로그도 없이 화면에 중복으로만 나타난다 (7.0.10 신규 설치 대시보드에서 실제 발생).
|
||||
*
|
||||
* @effects publish_failure_listener_is_registered_on_hook
|
||||
*/
|
||||
public function test_listener_is_registered_exactly_once(): void
|
||||
{
|
||||
$this->seedMarker(2);
|
||||
|
||||
$alerts = HookManager::applyFilters('core.dashboard.alerts', []);
|
||||
|
||||
$ids = array_count_values(array_column($alerts, 'id'));
|
||||
|
||||
$this->assertSame(1, $ids['static_publish_failure'] ?? 0, '정적 게시 실패 알림이 중복 등록되었다 — 같은 카드가 두 장 뜬다');
|
||||
}
|
||||
|
||||
/**
|
||||
* 마커 접근자가 서비스의 기록과 같은 키를 본다 (키 오타 회귀 가드).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Installer;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* 설치 마법사의 쓰기 권한 검사 목록에 정적 게시 루트의 상위(`public/build`)가 포함된다.
|
||||
*
|
||||
* 설치 마법사는 웹 계정으로 실행되므로 다른 계정 소유의 디렉토리 권한을 스스로 고칠 수 없다.
|
||||
* 할 수 있는 것은 `REQUIRED_DIRECTORIES` 를 검사해 통과할 때까지 진행을 막고 운영자가 실행할
|
||||
* 명령을 보여 주는 것뿐이다. 그 목록에서 `public/build` 가 빠지면 마법사는 막지도 안내하지도
|
||||
* 않은 채 설치를 진행하고, 마지막 단계의 정적 게시가 `parent_not_writable` 로 실패한 뒤 대시보드에
|
||||
* 「초기 화면 파일 생성 실패」 알림이 뜬다 (7.0.10 신규 설치에서 실제 발생).
|
||||
*/
|
||||
class RequiredDirectoriesPublishRootTest extends TestCase
|
||||
{
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
$projectRoot = dirname(__DIR__, 3);
|
||||
|
||||
if (! defined('BASE_PATH')) {
|
||||
define('BASE_PATH', $projectRoot);
|
||||
}
|
||||
|
||||
if (! isset($_SERVER['SCRIPT_NAME'])) {
|
||||
$_SERVER['SCRIPT_NAME'] = '/install/index.php';
|
||||
}
|
||||
|
||||
foreach (['PrivilegedDatabaseAccounts', 'OpcacheStatus'] as $supportClass) {
|
||||
if (! class_exists('App\\Support\\'.$supportClass, false)) {
|
||||
require_once $projectRoot.'/app/Support/'.$supportClass.'.php';
|
||||
}
|
||||
}
|
||||
|
||||
require_once $projectRoot.'/public/install/includes/config.php';
|
||||
}
|
||||
|
||||
public function test_required_directories_include_static_publish_root_parent(): void
|
||||
{
|
||||
$this->assertArrayHasKey(
|
||||
'public/build',
|
||||
REQUIRED_DIRECTORIES,
|
||||
'설치 마법사가 public/build 쓰기 권한을 검사하지 않는다 — 정적 게시가 설치 마지막 단계에서 조용히 실패한다'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_publish_root_parent_is_tracked_so_the_check_can_run(): void
|
||||
{
|
||||
$this->assertDirectoryExists(dirname(__DIR__, 3).'/public/build', 'public/build 가 저장소에 없으면 검사 대상 자체가 사라진다');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user