diff --git a/.env.example b/.env.example index 8fcf8120..2db6325b 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ APP_ENV=production APP_KEY= APP_DEBUG=false APP_URL=http://localhost -APP_VERSION=7.0.2 +APP_VERSION=7.0.3 APP_LOCALE=ko APP_FALLBACK_LOCALE=ko diff --git a/.env.testing.example b/.env.testing.example index 96277f5a..7a637226 100644 --- a/.env.testing.example +++ b/.env.testing.example @@ -3,7 +3,7 @@ APP_ENV=testing APP_KEY= APP_DEBUG=false APP_URL=http://localhost -APP_VERSION=7.0.2 +APP_VERSION=7.0.3 APP_LOCALE=ko APP_FALLBACK_LOCALE=ko diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e6c423..ba8bb30c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [7.0.3] - 2026-07-08 + +### Fixed + +- 이미 설치를 마친 환경의 설정 파일(`.env`)을 새 서버로 복사해, 데이터베이스가 아직 비어 있는 상태에서 설치를 이어가려 할 때 `php artisan migrate`로 테이블을 만들기도 전에 부팅 단계에서 오류가 나며 중단되던 문제를 수정했습니다. 이제 데이터베이스 작업 명령이 실행되는 동안에는 테이블 존재 여부를 실제로 확인하므로, 기존 서버의 설정을 재사용하는 신규 배포에서도 설치가 정상적으로 진행됩니다. (#41 @GyusoonKim 님께서 제보해주셨습니다.) + ## [7.0.2] - 2026-07-08 ### Added diff --git a/INSTALL.md b/INSTALL.md index 4c4d2f2b..ad65f5a1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -246,7 +246,7 @@ unzip g7-release.zip # 압축 해제 결과 확인 — 루트 디렉토리가 g7이 아니면 이름 변경 ls -la -# (필요 시) mv g7-7.0.2 g7 +# (필요 시) mv g7-7.0.3 g7 # ZIP 파일 정리 (선택) rm g7-release.zip diff --git a/README.md b/README.md index a5929011..afb587e2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
-
+
diff --git a/app/Extension/Traits/CachesModuleStatus.php b/app/Extension/Traits/CachesModuleStatus.php
index 95cd4c49..73a1b88e 100644
--- a/app/Extension/Traits/CachesModuleStatus.php
+++ b/app/Extension/Traits/CachesModuleStatus.php
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
use App\Enums\ExtensionStatus;
use App\Extension\Cache\CoreCacheDriver;
use App\Models\Module;
+use App\Support\InstallerContext;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@@ -63,8 +64,6 @@ trait CachesModuleStatus
/**
* 모듈 상태 캐시를 무효화합니다.
* 모듈 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
- *
- * @return void
*/
public static function invalidateModuleStatusCache(): void
{
@@ -79,10 +78,14 @@ trait CachesModuleStatus
* 설치 완료 상태(`config('app.installer_completed')`)일 때는 테이블 존재를
* 전제로 하여 `Schema::hasTable()` 호출을 건너뜁니다. 인스톨러 이전 환경이나
* 테스트에서는 기존 체크 경로로 폴백합니다.
+ *
+ * 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
+ * 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
+ * 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
*/
private static function isExtensionTableReady(string $table): bool
{
- if (config('app.installer_completed')) {
+ if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
return true;
}
diff --git a/app/Extension/Traits/CachesPluginStatus.php b/app/Extension/Traits/CachesPluginStatus.php
index e6c1f172..de1caa82 100644
--- a/app/Extension/Traits/CachesPluginStatus.php
+++ b/app/Extension/Traits/CachesPluginStatus.php
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
use App\Enums\ExtensionStatus;
use App\Extension\Cache\CoreCacheDriver;
use App\Models\Plugin;
+use App\Support\InstallerContext;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@@ -63,8 +64,6 @@ trait CachesPluginStatus
/**
* 플러그인 상태 캐시를 무효화합니다.
* 플러그인 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
- *
- * @return void
*/
public static function invalidatePluginStatusCache(): void
{
@@ -76,10 +75,14 @@ trait CachesPluginStatus
/**
* 설치 완료 상태에서는 `Schema::hasTable()` 호출을 건너뜁니다.
* 인스톨러 이전 환경이나 테스트에서는 기존 체크 경로로 폴백합니다.
+ *
+ * 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
+ * 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
+ * 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
*/
private static function isPluginTableReady(): bool
{
- if (config('app.installer_completed')) {
+ if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
return true;
}
diff --git a/app/Extension/Traits/CachesTemplateStatus.php b/app/Extension/Traits/CachesTemplateStatus.php
index 10294afd..746e06cd 100644
--- a/app/Extension/Traits/CachesTemplateStatus.php
+++ b/app/Extension/Traits/CachesTemplateStatus.php
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
use App\Enums\ExtensionStatus;
use App\Extension\Cache\CoreCacheDriver;
use App\Models\Template;
+use App\Support\InstallerContext;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@@ -86,8 +87,6 @@ trait CachesTemplateStatus
/**
* 템플릿 상태 캐시를 무효화합니다.
* 템플릿 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
- *
- * @return void
*/
public static function invalidateTemplateStatusCache(): void
{
@@ -101,10 +100,14 @@ trait CachesTemplateStatus
/**
* 설치 완료 상태에서는 `Schema::hasTable()` 호출을 건너뜁니다.
* 인스톨러 이전 환경이나 테스트에서는 기존 체크 경로로 폴백합니다.
+ *
+ * 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
+ * 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
+ * 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
*/
private static function isTemplateTableReady(): bool
{
- if (config('app.installer_completed')) {
+ if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
return true;
}
diff --git a/app/Providers/LanguagePackServiceProvider.php b/app/Providers/LanguagePackServiceProvider.php
index fe30659a..3454ef02 100644
--- a/app/Providers/LanguagePackServiceProvider.php
+++ b/app/Providers/LanguagePackServiceProvider.php
@@ -18,6 +18,7 @@ use App\Services\LanguagePack\LanguagePackBundledRegistrar;
use App\Services\LanguagePack\LanguagePackRegistry;
use App\Services\LanguagePack\LanguagePackSeedInjector;
use App\Services\LanguagePack\LanguagePackTranslator;
+use App\Support\InstallerContext;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\File;
@@ -477,8 +478,7 @@ class LanguagePackServiceProvider extends ServiceProvider
return false;
}
- if ($this->app->runningInConsole()
- && in_array($_SERVER['argv'][1] ?? null, ['migrate', 'migrate:fresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'], true)) {
+ if (InstallerContext::isSchemaMutatingCommand()) {
return false;
}
diff --git a/app/Providers/ModuleRouteServiceProvider.php b/app/Providers/ModuleRouteServiceProvider.php
index 658610b2..767ebf09 100644
--- a/app/Providers/ModuleRouteServiceProvider.php
+++ b/app/Providers/ModuleRouteServiceProvider.php
@@ -6,6 +6,7 @@ use App\Enums\ExtensionStatus;
use App\Extension\ExtensionManager;
use App\Extension\Testing\ExtensionTestAllowlist;
use App\Models\Module;
+use App\Support\InstallerContext;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
@@ -52,7 +53,12 @@ class ModuleRouteServiceProvider extends ServiceProvider
// 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
// 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
- if (! config('app.installer_completed')) {
+ //
+ // 단, 마이그레이션 계열 명령 실행 중에는 INSTALLER_COMPLETED=true 라도 테이블이
+ // 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
+ // 무력화하고 hasTable 검증 경로로 진입한다. 빈 DB 면 여기서 early return 하여
+ // 아래 무방비 pluck 이 table not found 로 부팅을 깨뜨리는 것을 막는다.
+ if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
try {
if (! Schema::hasTable('modules')) {
return;
diff --git a/app/Providers/PluginRouteServiceProvider.php b/app/Providers/PluginRouteServiceProvider.php
index d29a1838..0798c3a3 100644
--- a/app/Providers/PluginRouteServiceProvider.php
+++ b/app/Providers/PluginRouteServiceProvider.php
@@ -4,6 +4,7 @@ namespace App\Providers;
use App\Extension\ExtensionManager;
use App\Extension\Testing\ExtensionTestAllowlist;
+use App\Support\InstallerContext;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
@@ -48,7 +49,12 @@ class PluginRouteServiceProvider extends ServiceProvider
// 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
// 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
- if (! config('app.installer_completed')) {
+ //
+ // 단, 마이그레이션 계열 명령 실행 중에는 INSTALLER_COMPLETED=true 라도 테이블이
+ // 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
+ // 무력화하고 hasTable 검증 경로로 진입한다. 빈 DB 면 여기서 early return 하여
+ // 이후 플러그인 부팅/상태 조회가 table not found 로 부팅을 깨뜨리는 것을 막는다.
+ if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
try {
if (! Schema::hasTable('plugins')) {
return;
diff --git a/app/Support/InstallerContext.php b/app/Support/InstallerContext.php
new file mode 100644
index 00000000..b3c569e2
--- /dev/null
+++ b/app/Support/InstallerContext.php
@@ -0,0 +1,40 @@
+runningInConsole()) {
+ return false;
+ }
+
+ return in_array(
+ $_SERVER['argv'][1] ?? null,
+ ['migrate', 'migrate:fresh', 'migrate:refresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'],
+ true
+ );
+ }
+}
diff --git a/config/app.php b/config/app.php
index a7d6823b..4885bc61 100644
--- a/config/app.php
+++ b/config/app.php
@@ -231,7 +231,7 @@ return [
|
*/
- 'version' => env('APP_VERSION', '7.0.2'),
+ 'version' => env('APP_VERSION', '7.0.3'),
/*
|--------------------------------------------------------------------------
diff --git a/docs/backend/service-provider.md b/docs/backend/service-provider.md
index aefd901d..57f63a36 100644
--- a/docs/backend/service-provider.md
+++ b/docs/backend/service-provider.md
@@ -334,11 +334,14 @@ protected function loadModuleRoutes(): void
### 동작 표
-| 환경 | `installer_completed` | 동작 |
-|------|----------------------|------|
-| 프로덕션 (설치 완료) | `true` | 가드 통과 → `hasTable` 스킵 (쿼리 0건) |
-| 인스톨러 실행 중 / 마이그레이션 전 | `false` (기본값) | 기존 `hasTable` 폴백 경로 (원본 동작 보존) |
-| 테스트 (`.env.testing` 에 플래그 없음) | `false` | 기존 `hasTable` 경로 |
+| 환경 | `installer_completed` | 마이그레이션 명령 | 동작 |
+|------|----------------------|------------------|------|
+| 프로덕션 (설치 완료, 웹 요청) | `true` | 아님 | 가드 통과 → `hasTable` 스킵 (쿼리 0건) |
+| 설치 완료 `.env` 복사 + 빈 DB (마이그레이션 전) | `true` | **실행 중** | fast-path 무력화 → `hasTable` 폴백 (table not found 부팅 실패 방지) |
+| 인스톨러 실행 중 / 마이그레이션 전 | `false` (기본값) | — | 기존 `hasTable` 폴백 경로 (원본 동작 보존) |
+| 테스트 (`.env.testing` 에 플래그 없음) | `false` | — | 기존 `hasTable` 경로 |
+
+두 번째 행이 이 가드의 핵심 케이스다. `INSTALLER_COMPLETED=true` 가 적힌 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을 만들기 **전** 에 앱이 부팅되면, fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는 쿼리가 "table not found" 로 부팅을 깨뜨린다. 이 컨텍스트에서는 fast-path 를 신뢰하지 않고 실제 `hasTable` 검증 경로로 폴백해야 한다 (아래 "마이그레이션 컨텍스트 가드" 참조).
### 적용 가이드
@@ -346,6 +349,66 @@ protected function loadModuleRoutes(): void
- **`hasTable` 폴백 경로는 반드시 유지** — `installer_completed=false` 환경에서도 안전하게 부팅되어야 함
- **try/catch 도 그대로 유지** — DB 연결 실패 시 안전한 스킵 계약 준수
- 가드는 hasTable 체크 **전체 블록을 `if (! config('app.installer_completed'))` 로 래핑** 하는 형태
+- **fast-path 이후 쿼리가 try/catch 로 보호되지 않는 지점은 마이그레이션 컨텍스트 가드를 함께 적용** (아래 참조). 예외로 보호되는 지점은 빈 DB 에서도 안전하므로 불필요
+
+### 마이그레이션 컨텍스트 가드
+
+`installer_completed` fast-path 는 테이블 존재를 전제한다. 그러나 `INSTALLER_COMPLETED=true` 가 적힌 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을 만들기 **전** 에 앱이 부팅되면, fast-path 가 `true` 를 반환하고 곧이어 실제 쿼리(`Model::where()->pluck()` 등)가 실행되어 "table not found" 로 부팅이 실패한다. 마이그레이션을 실행하려는 부팅에서 그 명령이 오히려 실패하는 닭-달걀 상태다.
+
+이를 막기 위해, 스키마를 파괴/재생성하는 마이그레이션 계열 콘솔 명령 판정을 `App\Support\InstallerContext::isSchemaMutatingCommand()` 로 단일 SSoT 화하고, fast-path 지점에 가드로 결합한다. 명령 리스트: `migrate`, `migrate:fresh`, `migrate:refresh`, `migrate:rollback`, `migrate:reset`, `db:wipe`.
+
+```php
+// App\Support\InstallerContext
+public static function isSchemaMutatingCommand(): bool
+{
+ if (! app()->runningInConsole()) {
+ return false;
+ }
+
+ return in_array(
+ $_SERVER['argv'][1] ?? null,
+ ['migrate', 'migrate:fresh', 'migrate:refresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'],
+ true
+ );
+}
+```
+
+가드 결합 방식은 지점의 **논리 극성** 에 따라 다르다:
+
+- **Trait (fast-path 통과 조건)** — `&&` 로 결합. 마이그레이션 중이면 fast-path 를 건너뛰고 hasTable 폴백으로 진입.
+
+ ```php
+ if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
+ return true;
+ }
+ ```
+
+- **RouteServiceProvider (hasTable 검증 블록 진입 조건)** — `||` 로 결합. 마이그레이션 중이면 hasTable 검증 경로로 진입해 빈 DB 시 early return → 무방비 `pluck` 도달 차단.
+
+ ```php
+ if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
+ try {
+ if (! Schema::hasTable('modules')) {
+ return;
+ }
+ // ...
+ } catch (\Exception) {
+ return;
+ }
+ }
+ ```
+
+`isRegistryReady()` 처럼 fast-path 앞에서 조기 반환하는 지점은 `if (InstallerContext::isSchemaMutatingCommand()) { return false; }` 로 단순 게이트한다.
+
+#### 부류 A (가드 필수) vs 부류 B (불필요)
+
+| 부류 | 조건 | 예 | 조치 |
+|------|------|-----|------|
+| A | fast-path 이후 쿼리가 try/catch 로 **미보호** → 빈 DB 에서 부팅 실패 | `Caches{Module,Plugin,Template}Status`, `Module/PluginRouteServiceProvider` | 마이그레이션 컨텍스트 가드 적용 |
+| B | fast-path 이후 쿼리가 try/catch(\Throwable) 로 **보호** → 예외를 삼켜 안전한 기본값 반환 | `NotificationHookListener`, `IdentityPolicyRepository::{listHookTargets,activeExtensionIdentifiers}` | 가드 불필요 (예외 흡수로 안전) |
+
+### config 캐시 재생성 주의
+
- `config:cache` 는 config 소스를 변경하는 라이프사이클에서 `App\Support\ConfigCacheHelper::rebuild()` 로 자동 재생성된다 (아래 "config 캐시 자동 재생성" 참조). `.env` 를 직접 편집한 경우처럼 헬퍼를 거치지 않는 변경은 여전히 `php artisan config:clear && php artisan config:cache` 를 수동 실행한다
### 확장 Trait 패턴
diff --git a/tests/Unit/Extension/InstallerCompletedGuardTest.php b/tests/Unit/Extension/InstallerCompletedGuardTest.php
index ace72b89..a5164f6c 100644
--- a/tests/Unit/Extension/InstallerCompletedGuardTest.php
+++ b/tests/Unit/Extension/InstallerCompletedGuardTest.php
@@ -7,8 +7,11 @@ use App\Extension\ModuleManager;
use App\Extension\PluginManager;
use App\Extension\TemplateManager;
use App\Listeners\NotificationHookListener;
+use App\Providers\ModuleRouteServiceProvider;
+use App\Providers\PluginRouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
+use ReflectionClass;
use Tests\TestCase;
/**
@@ -22,12 +25,43 @@ class InstallerCompletedGuardTest extends TestCase
{
use RefreshDatabase;
+ /** @var array