fix(core): 신규 설치 후 대시보드 정적 게시 실패 알림 — 마법사 권한 검사 누락·알림 이중 등록 수정

- 설치 마법사 디렉토리 권한 검사 목록(REQUIRED_DIRECTORIES)에 public/build 추가
 (웹 계정이 쓸 수 없어도 막지 않고 진행해 설치 마지막 단계의 정적 게시가 실패했다)
- AppServiceProvider 의 대시보드 알림 리스너 수동 등록 제거 — CoreServiceProvider
 자동 발견(훅 캐시/스캔)과 이중 등록되어 같은 알림 카드가 두 장 표시됐다
- 회귀 테스트: RequiredDirectoriesPublishRootTest, StaticPublishAlertTest 중복 등록 단언
- INSTALL.md 마법사 안내 문장, CHANGELOG 7.0.10 Fixed 2건
This commit is contained in:
HeuJung
2026-09-06 22:44:24 +09:00
parent 7946410ba6
commit e3ca585afd
6 changed files with 78 additions and 44 deletions
+2
View File
@@ -52,6 +52,8 @@
### Fixed
- 설치 마법사의 디렉토리 권한 검사 단계(`storage`·`bootstrap/cache` 와 같은 단계)에서 `public/build` 가 검사 목록에 빠져 있던 문제를 수정했습니다. 웹 서버 계정이 그 폴더에 쓸 수 없어도 마법사가 막거나 실행할 명령을 보여 주지 않은 채 설치를 진행했고, 설치 마지막 단계의 초기 화면 파일 생성이 실패한 뒤 대시보드에 「초기 화면 파일 생성 실패」 알림이 떴습니다. 이제 다른 폴더와 같은 방식으로 통과할 때까지 진행을 막고 권한 부여 명령을 안내합니다.
- 관리자 대시보드의 시스템 알림(초기 화면 파일 생성 실패·확장 호환성·신뢰 프록시)이 같은 내용으로 두 장씩 표시되던 문제를 수정했습니다. 알림 리스너가 자동 발견과 별도 등록으로 두 번 등록된 것이 원인입니다.
- 새로 설치할 때 일부 확장의 설치가 "Class not found" 오류로 중단되던 문제를 수정했습니다. 확장의 설정 기본값을 읽는 단계가 그 확장의 클래스 경로 등록보다 먼저 실행된 것이 원인이며, 이제 설치는 확장 파일을 복사한 직후 클래스 경로를 먼저 등록합니다. 이미 설치된 사이트의 업데이트에는 영향이 없습니다.
- 설정 캐시가 만들어져 있는 사이트(설치 마법사·환경설정 저장·확장 업데이트를 한 번이라도 거친 대부분의 사이트)에서 코어 업데이트가 업그레이드 스텝 단계에서 "수동 재개" 안내와 함께 멈추던 문제를 수정했습니다. 새 버전으로 실행되는 스텝 프로세스가 이전 버전의 설정 캐시를 읽어 자기 자신을 옛 버전으로 오판한 것이 원인이며, 실행할 스텝이 하나도 없는 버전으로 올릴 때도 같은 안내가 나왔습니다. 같은 원인으로 새 버전이 추가한 쓰기 폴더가 `sudo` 업데이트의 권한 정리에서 빠지던 문제도 함께 바로잡았고, 이제 업데이트는 스텝 실행 전에 이전 설정 캐시를 비웁니다.
- 명령줄로 업그레이드 스텝을 직접 재실행하거나 관리자 「시스템 최적화」를 실행한 뒤, 그 실행이 예약한 초기 화면 정적 파일 재생성이 조용히 건너뛰어지던 문제를 수정했습니다.
+1 -1
View File
@@ -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 설치 방식 선택:**
-43
View File
@@ -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 프록시를 설정합니다.
*
+1
View File
@@ -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 가 저장소에 없으면 검사 대상 자체가 사라진다');
}
}