diff --git a/CHANGELOG.md b/CHANGELOG.md index 304cc600..f393be09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ ### Fixed +- 설치 마법사의 디렉토리 권한 검사 단계(`storage`·`bootstrap/cache` 와 같은 단계)에서 `public/build` 가 검사 목록에 빠져 있던 문제를 수정했습니다. 웹 서버 계정이 그 폴더에 쓸 수 없어도 마법사가 막거나 실행할 명령을 보여 주지 않은 채 설치를 진행했고, 설치 마지막 단계의 초기 화면 파일 생성이 실패한 뒤 대시보드에 「초기 화면 파일 생성 실패」 알림이 떴습니다. 이제 다른 폴더와 같은 방식으로 통과할 때까지 진행을 막고 권한 부여 명령을 안내합니다. +- 관리자 대시보드의 시스템 알림(초기 화면 파일 생성 실패·확장 호환성·신뢰 프록시)이 같은 내용으로 두 장씩 표시되던 문제를 수정했습니다. 알림 리스너가 자동 발견과 별도 등록으로 두 번 등록된 것이 원인입니다. - 새로 설치할 때 일부 확장의 설치가 "Class not found" 오류로 중단되던 문제를 수정했습니다. 확장의 설정 기본값을 읽는 단계가 그 확장의 클래스 경로 등록보다 먼저 실행된 것이 원인이며, 이제 설치는 확장 파일을 복사한 직후 클래스 경로를 먼저 등록합니다. 이미 설치된 사이트의 업데이트에는 영향이 없습니다. - 설정 캐시가 만들어져 있는 사이트(설치 마법사·환경설정 저장·확장 업데이트를 한 번이라도 거친 대부분의 사이트)에서 코어 업데이트가 업그레이드 스텝 단계에서 "수동 재개" 안내와 함께 멈추던 문제를 수정했습니다. 새 버전으로 실행되는 스텝 프로세스가 이전 버전의 설정 캐시를 읽어 자기 자신을 옛 버전으로 오판한 것이 원인이며, 실행할 스텝이 하나도 없는 버전으로 올릴 때도 같은 안내가 나왔습니다. 같은 원인으로 새 버전이 추가한 쓰기 폴더가 `sudo` 업데이트의 권한 정리에서 빠지던 문제도 함께 바로잡았고, 이제 업데이트는 스텝 실행 전에 이전 설정 캐시를 비웁니다. - 명령줄로 업그레이드 스텝을 직접 재실행하거나 관리자 「시스템 최적화」를 실행한 뒤, 그 실행이 예약한 초기 화면 정적 파일 재생성이 조용히 건너뛰어지던 문제를 수정했습니다. diff --git a/INSTALL.md b/INSTALL.md index 43259032..aedfdf78 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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 설치 방식 선택:** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f2b3c6e4..6e264c4b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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 프록시를 설정합니다. * diff --git a/public/install/includes/config.php b/public/install/includes/config.php index 4adf9907..3dc05d7d 100644 --- a/public/install/includes/config.php +++ b/public/install/includes/config.php @@ -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에서 설정) diff --git a/tests/Feature/Dashboard/StaticPublishAlertTest.php b/tests/Feature/Dashboard/StaticPublishAlertTest.php index 9a538bc8..00801cee 100644 --- a/tests/Feature/Dashboard/StaticPublishAlertTest.php +++ b/tests/Feature/Dashboard/StaticPublishAlertTest.php @@ -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, '정적 게시 실패 알림이 중복 등록되었다 — 같은 카드가 두 장 뜬다'); + } + /** * 마커 접근자가 서비스의 기록과 같은 키를 본다 (키 오타 회귀 가드). * diff --git a/tests/Unit/Installer/RequiredDirectoriesPublishRootTest.php b/tests/Unit/Installer/RequiredDirectoriesPublishRootTest.php new file mode 100644 index 00000000..0d2c2ce3 --- /dev/null +++ b/tests/Unit/Installer/RequiredDirectoriesPublishRootTest.php @@ -0,0 +1,54 @@ +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 가 저장소에 없으면 검사 대상 자체가 사라진다'); + } +}