fix(core,admin_basic): 정적 게시 재게시 누락 3건 수정 + 관리자 수동 복구 UI + sudo core:update 소유권 정합
- 템플릿 update 는 레이아웃 변경 여부와 무관하게 캐시 버전을 올리고(실패 복원 뒤에도), 자산 주소 방식 전환 3경로도 bump 한다 - config:cache 가 컨테이너 인스턴스를 덮어 이후 terminating 재게시가 사라지던 결함을 복원 헬퍼로 차단 - custom/ 변경 감지와 게시가 같은 열거자(재귀·크기 포함)를 쓰고, 서명은 호스트별로 저장 - 캐시 버전·서명 키를 만료시키지 않는다(기본 TTL 24h 로 매일 전체 재생성되던 문제) - 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드: 상태 조회 + 지금 다시 만들기(확인 모달), 대시보드 알림 버튼 연결, CLI 와 같은 statusReport 소비 - sudo core:update 경로의 root 소유 잔존(설정 디렉토리·임시 폴더·로그·업그레이드 마이그레이션 산출물) 상속·정합화, ext-static 명령 root 경고와 sudo -u 힌트 - 안내·문서·트러블슈팅 4건·audit 룰 ext-cache-version-raw-read·ja 언어팩 동기
This commit is contained in:
1 parent
d263f74cae
commit
de18159986
105 files changed
+4055
-208
No files matched your search
@@ -254,7 +254,7 @@ CSS 가 아닌 자산은 바이트 그대로 서빙됩니다. 정적 게시본(`
|
||||
| [schedules.md](schedules.md) | `schedules` | 12 |
|
||||
| [search.md](search.md) | `search` | 1 |
|
||||
| [seo.md](seo.md) | `seo` | 5 |
|
||||
| [settings.md](settings.md) | `settings` | 16 |
|
||||
| [settings.md](settings.md) | `settings` | 18 |
|
||||
| [system.md](system.md) | `system` | 2 |
|
||||
| [templates.md](templates.md) | `templates` | 57 |
|
||||
| [users.md](users.md) | `users` | 12 |
|
||||
|
||||
@@ -1127,6 +1127,169 @@ HTTP/1.1 200
|
||||
서버 실행 환경 정보를 한 번에 조회합니다. OS/웹서버/PHP/DB/Laravel/코어 버전, CPU·메모리·디스크 사용량, PHP 주요 설정값(memory_limit·max_execution_time·upload_max_filesize), 주요 경로, PHP 확장 로드 상태, DB 연결 구성 요약 등을 포함합니다. 관리자 시스템 정보 화면과 요구사항 점검용 진단 데이터로 사용됩니다.
|
||||
|
||||
|
||||
### GET /api/admin/settings/static-cache
|
||||
<!-- @generated:start:api.admin.settings.static-cache -->
|
||||
- **라우트명**: `api.admin.settings.static-cache`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@staticCacheStatus`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/settings/static-cache HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| enabled | boolean | `true` | kill-switch(`G7_STATIC_CACHE`) 상태. `false` 면 게시하지 않고 모든 화면이 API 로 동작한다 |
|
||||
| publishable | boolean | `true` | 지금 이 설치에서 게시가 실제로 쓰이는지 — `enabled` 이고 운영 모드(`APP_ENV=production`)일 때만 `true`. 화면의 [지금 다시 만들기] 활성 조건 |
|
||||
| environment | string | `production` | 현재 앱 환경 |
|
||||
| version | integer | `1788656566` | 현재 확장 캐시 버전 (정적 게시 디렉토리 이름). 키가 없으면 새로 만들어 돌려주므로 항상 유효값 |
|
||||
| published | boolean | `true` | 현재 버전의 게시 완료(manifest 존재) 여부 |
|
||||
| files | integer | `714` | manifest 에 기록된 게시 파일 수 (미게시면 0) |
|
||||
| published_at | string\|null | `2026-09-06T01:02:49+00:00` | 마지막 게시 시각 (ISO 8601, 미게시면 `null`) |
|
||||
| tree_writable | boolean | `true` | 게시 트리(`public/build/ext`)에 쓸 수 있는지 — 없으면 부모에 만들 수 있는지 |
|
||||
| process_user | string | `www-data` | 웹 프로세스 계정명 (posix 미지원 환경은 `unknown`). 게시 산출물의 소유자가 될 계정 |
|
||||
| failure | object\|null | `null` | 최근 게시 실패 마커 `{version, at, reason, count, message}`. `reason` 은 `parent_not_writable` / `write_failed` / `lock_unavailable` |
|
||||
| retained_versions | array | `[1788171201,1788656566]` | 게시 루트에 남아 있는 버전 디렉토리 목록 (오름차순). 정상은 현재 + 직전 1개 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "초기 화면 정적 파일 상태를 불러왔습니다.",
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"publishable": true,
|
||||
"environment": "production",
|
||||
"version": 1788656566,
|
||||
"published": true,
|
||||
"files": 714,
|
||||
"published_at": "2026-09-06T01:02:49+00:00",
|
||||
"tree_writable": true,
|
||||
"process_user": "www-data",
|
||||
"failure": null,
|
||||
"retained_versions": [
|
||||
1788171201,
|
||||
1788656566
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
부트스트랩 리소스 정적 게시(초기 화면 정적 파일)의 상태를 돌려줍니다. 판정은 서비스 한 곳이 소유하며 CLI `php artisan ext-static:status` 와 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드가 같은 값을 소비합니다. 읽기 전용이며 입력이 없습니다.
|
||||
|
||||
`publishable` 이 `false` 이면 게시가 쓰이지 않는 환경(kill-switch 꺼짐 또는 비프로덕션)이라 화면은 [지금 다시 만들기] 를 비활성으로 두고 그 이유를 안내합니다. `failure` 는 게시가 거듭 실패했을 때만 채워지며, 대시보드 알림과 같은 마커를 읽습니다. 상세: [static-asset-publishing.md](../static-asset-publishing.md) "5-1. 관리자 화면에서의 수동 복구".
|
||||
|
||||
|
||||
### POST /api/admin/settings/static-cache/republish
|
||||
<!-- @generated:start:api.admin.settings.static-cache.republish -->
|
||||
- **라우트명**: `api.admin.settings.static-cache.republish`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@republishStaticCache`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/settings/static-cache/republish HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드 — 아래 4개 + `GET /api/admin/settings/static-cache` 의 상태 보고서 필드 전부._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| republished | boolean | `true` | 새 버전 게시가 완료됐는지. `false` 여도 HTTP 200 으로 응답한다 — 게시 실패는 요청 처리 실패가 아니라 진단 결과다 (사이트는 API 폴백으로 정상) |
|
||||
| version_changed | boolean | `true` | 캐시 버전이 이전과 달라졌는지. 같은 초에 연속 호출하면 `false` 일 수 있으나 내용은 강제로 다시 만들어진다 |
|
||||
| previous_version | integer | `1788656566` | 호출 전 캐시 버전 |
|
||||
| reason | string\|null | `null` | `republished=false` 일 때의 사유. `disabled`(kill-switch 꺼짐) / `not_production`(비프로덕션 — 이 둘은 버전을 올리지 않는다) / 실패 마커 사유(`parent_not_writable` · `write_failed` · `lock_unavailable`) / `publish_failed` |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "초기 화면 정적 파일을 다시 만들었습니다.",
|
||||
"data": {
|
||||
"republished": true,
|
||||
"version_changed": true,
|
||||
"previous_version": 1788656566,
|
||||
"reason": null,
|
||||
"enabled": true,
|
||||
"publishable": true,
|
||||
"environment": "production",
|
||||
"version": 1788657022,
|
||||
"published": true,
|
||||
"files": 714,
|
||||
"published_at": "2026-09-06T01:10:22+00:00",
|
||||
"tree_writable": true,
|
||||
"process_user": "www-data",
|
||||
"failure": null,
|
||||
"retained_versions": [
|
||||
1788656566,
|
||||
1788657022
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
초기 화면 정적 파일을 지금 다시 만듭니다(관리자 수동 복구). 확장 캐시 버전을 올리고 현재 버전을 **강제** 재게시하므로, 같은 초에 다시 호출해도 내용을 새로 만듭니다. 캐시 버전은 만료로 재생성되지 않으므로(영구 번호) 재게시 트리거가 빠진 상태의 안전망이 이 엔드포인트입니다 — 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」의 [지금 다시 만들기] 와 대시보드 「초기 화면 파일 생성 실패」 알림의 [다시 만들기] 가 호출합니다.
|
||||
|
||||
게시가 쓰이지 않는 환경(`publishable=false`)에서는 버전을 올리지 않고 `republished=false` + `reason` 으로 돌아옵니다. 게시 실패도 HTTP 200 으로 돌아오며(연결 테스트·드라이버 테스트와 같은 규약) 실패 마커가 `failure` 에 실립니다.
|
||||
|
||||
715파일·40MB 복사와 확장 번들 재병합이 요청 안에서 동기로 수행됩니다. 서버는 실행 시간을 게시 락 TTL(300초)만큼 확보하고 클라이언트가 끊어도 게시를 끝내지만, FPM·nginx 타임아웃이 그보다 짧으면 응답이 먼저 끊길 수 있습니다 — 그 경우에도 게시는 계속되므로 상태 조회로 결과를 확인합니다.
|
||||
|
||||
|
||||
### GET /api/admin/settings/trusted-proxy
|
||||
<!-- @generated:start:api.admin.settings.trusted-proxy -->
|
||||
- **라우트명**: `api.admin.settings.trusted-proxy`
|
||||
|
||||
@@ -781,9 +781,15 @@ php artisan core:check-updates
|
||||
'backup_only' => ['vendor'], // 백업/복원 전용 (applyUpdate 제외)
|
||||
'backup_extra' => [...], // 추가 백업 대상
|
||||
'excludes' => [...], // 제외 패턴
|
||||
'restore_ownership' => [...], // sudo 실행 후 소유권을 원상 복원할 경로 (Step 11)
|
||||
'restore_ownership_group_writable' => [...], // 복원 직후 그룹 쓰기(g+w)까지 동기화할 경로
|
||||
],
|
||||
```
|
||||
|
||||
`restore_ownership` 복원은 흐름 **중간**(Step 11)이므로 그 뒤에 만들어지는 런타임 산출물은 흐름 **마지막**의 런타임 소유권 정상화가 덮는다. 대상은 다섯 곳이다: `storage/framework/cache`(캐시 키 인덱스·락 샤드), `bootstrap/cache`, `storage/app/ext-bundles`(병합 번들), `storage/app/temp`(확장 업데이트 임시 폴더 — 부모가 root 로 최초 생성되면 이후 관리자 화면의 확장 업데이트가 실패한다), `storage/logs`(daily 롤오버·신규 로그 파일). `storage/app/{modules,plugins}` 는 사용자 데이터 영역이라 의도적으로 제외되어 있으므로, 그 아래에 파일·디렉토리를 만드는 코드(설정 시드·업그레이드 마이그레이션)는 스스로 부모 소유권을 상속시킨다.
|
||||
|
||||
`.env` 에서 `G7_UPDATE_EXCLUDES` · `G7_UPDATE_TARGETS` · `G7_UPDATE_PROTECTED_PATHS` · `G7_UPDATE_RESTORE_OWNERSHIP` · `G7_UPDATE_RESTORE_OWNERSHIP_GROUP_WRITABLE` 로 재정의할 수 있다. 재정의 값은 기본 목록을 **통째로 대체**하므로 전체 목록을 다시 적는다 — 예를 들어 `G7_UPDATE_EXCLUDES` 에서 `build/ext` 가 빠지면 `--prune` 업데이트가 정적 게시본을 지운다. 기본값은 `.env.example` 에 주석으로 실려 있다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 에러 처리 및 로깅
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 게시물: public/build/ext/{cache_version}/ — 수명주기 이벤트와 운영자 custom 파일 변경마다 terminating 훅이 재생성 (활성 템플릿 + 활성 모듈·플러그인 custom + 병합 번들)
|
||||
1. 게시물: public/build/ext/{cache_version}/ — 수명주기 이벤트·자산 URL 방식 변경·운영자 custom 파일 변경(하위 폴더 포함)마다 terminating 훅이 재생성 (활성 템플릿 + 활성 모듈·플러그인 custom + 병합 번들)
|
||||
2. 서빙 게이트 3조건: 프로덕션 + G7_STATIC_CACHE(기본 on) + 게시 완료(manifest 존재)
|
||||
3. 폴백 2층: 태그 계층은 파일 단위 file_exists + 파샬 역변환, fetch 계층은 fetchStaticFirst
|
||||
4. 무효화는 버전 디렉토리 — 포인터(cache_version)가 바뀔 뿐 파일 덮어쓰기가 없다
|
||||
4. 무효화는 버전 디렉토리 — 포인터(cache_version)가 바뀔 뿐 파일 덮어쓰기가 없다. 버전은 만료되지 않는다 — 재게시는 변경이 있을 때만, 누락은 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」의 [지금 다시 만들기] 로 복구
|
||||
5. .json/.js/.css 로 끝나는 신규 동적(Laravel) 라우트를 만들지 않는다 — 실파일만 정적 확장자
|
||||
```
|
||||
|
||||
@@ -64,6 +64,13 @@ public/build/ext/{cache_version}/
|
||||
다시 해석하므로 고친 내용이 그 화면부터 반영됩니다(게시본이 아직 없는 동안에는 API 경로로
|
||||
떨어지고, 그 응답이 디스크 최신 내용을 줍니다).
|
||||
|
||||
감지 범위는 **게시 범위와 같은 열거자**가 정합니다 — `custom/` 아래를 재귀로 훑어 허용 확장자
|
||||
전부(CSS·JS 뿐 아니라 글꼴·이미지)의 경로·수정 시각·크기를 서명 재료로 씁니다. 그래서 CSS 는
|
||||
그대로 두고 `url('./fonts/x.woff2')` 가 가리키는 글꼴만 교체해도 다시 게시됩니다. 크기를 함께
|
||||
보는 이유는 수정 시각 해상도 안의 재작성과 시각을 보존하는 복사(`rsync -t`)를 잡기 위해서입니다.
|
||||
로드 목록(최상위 CSS·JS 만 페이지에 싣는 규약)은 그대로입니다 — 글꼴·이미지는 CSS 가 참조하는
|
||||
대상이지 그 자체로 로드할 것이 아닙니다.
|
||||
|
||||
모듈·플러그인은 **활성 확장의 `custom/` 만** 게시합니다 — 자산 서빙이 활성 확장에만 응답하므로
|
||||
비활성 확장의 파일을 게시해 봐야 아무도 참조하지 않는 사본이 버전 디렉토리마다 쌓입니다.
|
||||
확장의 빌드 산출물은 개별 파일이 아니라 병합 번들로 게시되므로, `custom/` 이 그 확장에서
|
||||
@@ -95,9 +102,17 @@ public/build/ext/{cache_version}/
|
||||
|---|---|---|
|
||||
| 수명주기 전체 | `incrementExtensionCacheVersion()` 내부 | terminating 게시 예약 — 프로세스당 1회, **실행 시점의 최종 버전**으로 게시 (연속 bump 자연 병합) |
|
||||
| 자가 치유 | blade 렌더의 `staticExtBase()` 게이트 | 현재 버전 미게시 감지 시 terminating 게시 예약. 이번 응답은 API URL — 아래 「자가 치유 창의 실제 비용」 참조 |
|
||||
| 수동/워밍 | `php artisan ext-static:publish [--force]` | 설치기 완료 단계에서도 호출 |
|
||||
| 수동/워밍 | `php artisan ext-static:publish [--force]` (웹 계정으로) | 설치기 완료 단계에서도 호출. 관리자 화면의 [지금 다시 만들기](§5-1)가 같은 일을 한다 |
|
||||
| GC | `php artisan ext-static:cleanup` + 게시 성공 직후 인라인 GC | 현재 + 직전 1개 보존. 스케줄 일 1회 등록 |
|
||||
|
||||
게시 입력이 되는 **설정값 변경**도 수명주기와 같은 단일 지점을 탄다. `general.asset_url_mode`(자산 URL 방식)는 병합 번들 CSS 안의 `url()` 형태로 본문에 구워지므로, 관리자 화면 저장·단건 저장·설정 복원·`g7:asset-url-mode` 어느 경로로 바뀌어도 확장 캐시 버전이 올라 번들과 게시본이 다시 만들어진다. 템플릿 업데이트는 레이아웃이 바뀌지 않아도(다국어·라우트·컴포넌트·dist 만 바뀐 릴리스) 모듈·플러그인 업데이트와 같이 **무조건** 버전을 올린다.
|
||||
|
||||
### 버전은 만료되지 않는다
|
||||
|
||||
확장 캐시 버전 키는 영구(10년) TTL 로 저장된다. 재게시는 수명주기 이벤트·설정 변경·custom 파일 변경·관리자 수동 복구·`php artisan cache:clear`(키 소실 → 재생성) 에서만 일어난다. 종전에는 키가 기본 TTL(24시간)로 만료되어 다음 방문마다 새 버전이 만들어졌고, 그것은 정적 파일 전체(실측 715파일·40MB) 재생성과 전 방문자의 자산 URL 변경이었다 — 안전망이 아니라 우발이었다.
|
||||
|
||||
버전이 만료되지 않으므로 재게시 트리거가 하나라도 빠지면 게시본은 무기한 stale 이 된다. 그 안전망이 §5-1 의 관리자 수동 복구다. `.env` 전용 값(`APP_FALLBACK_LOCALE` 등)을 바꿨을 때도 같은 통로로 다시 만든다.
|
||||
|
||||
### 자가 치유 창의 실제 비용
|
||||
|
||||
확장 수명주기 작업(설치·활성화·비활성화·삭제·업데이트)은 CLI 에서 캐시 버전만 올리고 게시는 다음 웹 렌더에 위임한다. 그래서 그 **직후 첫 페이지 로드 1회**는 API URL 로 나간다.
|
||||
@@ -111,7 +126,7 @@ public/build/ext/{cache_version}/
|
||||
|
||||
기능이 사라지지는 않는다(조작 수단인 아이콘은 인라인이라 영향 없음). 화면이 한 번 다른 글꼴로 보이고, 그 뒤 게시가 끝나면 정상으로 돌아온다.
|
||||
|
||||
이 창을 없애려면 수명주기 작업 뒤에 `php artisan ext-static:publish` 를 명시적으로 실행한다. 다만 그 명령을 **웹 계정이 아닌 사용자로** 돌리면 게시 산출물 소유권이 어긋날 수 있으므로, 위 「소유권」 절의 주의사항을 함께 본다.
|
||||
이 창을 없애려면 수명주기 작업 뒤에 `php artisan ext-static:publish` 를 **웹 계정으로**(`sudo -u {웹계정}`) 실행하거나, 관리자 > 환경설정 > 일반 의 [지금 다시 만들기](§5-1)를 누른다. 그 명령을 웹 계정이 아닌 사용자로 돌리면 게시 산출물 소유권이 어긋난다 — §6 참조.
|
||||
|
||||
- 동시성: 게시는 캐시 락으로 단일 실행. manifest 존재 시 skip(멱등).
|
||||
- 실패 정책: 쓰기 실패는 로그만 남기고 tmp 정리 — 사이트는 API 폴백으로 정상 ("정적 fast path 미적용" 상태이지 장애가 아니다). 다음 렌더의 자가 치유가 재시도한다.
|
||||
@@ -146,6 +161,25 @@ public/build/ext/{cache_version}/
|
||||
마커 자체를 쓰지 못하는 환경(캐시 저장소 불능)에서는 백오프가 걸리지 않는다 — best-effort 다. 그 경우에도 프로세스 안의 예약 가드는 그대로 유효해 한 요청 안에서 반복되지는 않는다.
|
||||
- routes 병합이 열화 상태(확장 업데이트 진행 중 등)면 그 산출물은 게시하지 않는다 — 정적 파일은 스스로 회복되지 않으므로 열화가 다음 bump 까지 박제된다. 같은 규율이 폴백 API 의 HTTP 캐시 헤더에도 적용된다 — 열화 응답에는 `public, max-age` 를 부여하지 않는다 (브라우저/CDN 박제 방지).
|
||||
|
||||
## 5-1. 관리자 화면에서의 수동 복구
|
||||
|
||||
관리자 > 환경설정 > 일반 의 「초기 화면 정적 파일」 카드가 게시 상태를 보여 주고, 즉시 다시 만들 수 있게 한다. 상태 판정은 서비스 한 곳(`ExtensionStaticCacheService::statusReport()`)이 소유하며 CLI(`ext-static:status`)·API·화면이 같은 값을 소비한다.
|
||||
|
||||
| 표시 | 뜻 |
|
||||
|---|---|
|
||||
| 상태 배지 | 게시됨 / 미게시 / 비활성(kill-switch 꺼짐) / 개발 모드(비프로덕션) |
|
||||
| 현재 버전 · 파일 수 · 마지막 게시 시각 | 현재 캐시 버전과 그 버전의 manifest 내용 |
|
||||
| 실행 계정 | 웹 프로세스 계정 — 게시 산출물의 소유자가 될 계정 |
|
||||
| 최근 실패 | 실패 마커(사유·연속 횟수). 사유별 조치 안내가 붙는다 |
|
||||
|
||||
[지금 다시 만들기] → 확인 모달 → `POST /api/admin/settings/static-cache/republish`. 서버는 캐시 버전을 올리고 현재 버전을 **강제** 재게시한다(같은 초에 다시 눌러도 내용을 다시 만든다). 게시 실패는 요청 실패가 아니라 진단 결과라 HTTP 200 + `republished=false` 로 돌아오고, 카드가 재조회되어 실패 사유를 보인다. 게시가 쓰이지 않는 환경(kill-switch 꺼짐·비프로덕션)에서는 버튼이 비활성이고 서버도 버전을 올리지 않는다. 권한은 조회 `core.settings.read`, 재게시 `core.settings.update`.
|
||||
|
||||
같은 엔드포인트가 대시보드의 「초기 화면 파일 생성 실패」 알림에도 [다시 만들기] 버튼으로 붙는다 — 운영자가 결함을 처음 만나는 곳에서 복구가 끝나도록.
|
||||
|
||||
715파일·40MB 복사와 번들 재병합이 **웹 요청 안에서** 돈다. 서버는 게시 락 TTL(300초)만큼 실행 시간을 확보하고 클라이언트가 끊어도 게시를 끝내지만, FPM `request_terminate_timeout`·nginx `fastcgi_read_timeout` 이 그보다 짧은 서버에서는 응답이 먼저 끊길 수 있다. 그 경우에도 게시는 계속되므로 카드를 다시 열어 결과를 확인한다.
|
||||
|
||||
이 통로가 있으므로 kill-switch 를 관리자 UI 로 두지 않는 방침(§5)은 그대로다 — 복구는 화면에서, 끄기는 서버에서.
|
||||
|
||||
## 5. 운영자 kill-switch
|
||||
|
||||
`.env` 에 `G7_STATIC_CACHE=false` 를 두면 게시가 중단되고 blade 가 정적 URL 을 방출하지 않아 전면 API 폴백(종전 동작)으로 돌아간다. 기본값은 활성이며 관리자 UI 는 두지 않는다 (내부 인프라 — 파일시스템 상태를 화면 토글이 즉시 반영한다고 오해할 소지가 있고, 문제 상황의 조치는 서버 접근을 전제한다).
|
||||
@@ -156,7 +190,7 @@ public/build/ext/{cache_version}/
|
||||
|
||||
- **웹 프로세스 계정의 `public/build` 쓰기 권한**: 게시의 정상 주체는 terminating 훅/자가 치유 = php-fpm(웹 계정)이다. 시스템 요구사항의 그룹 공유(방식 A) 구성이라면 `public/build` 도 같은 원칙(g+w + 공용 그룹)을 적용한다. 게시 코드가 디렉토리를 0775 로 생성하므로 umask 동조 환경에서 그룹 쓰기가 유지된다.
|
||||
- **설치 시**: 설치기 완료 단계가 `ext-static:publish --force` 를 best-effort 로 실행한다 — 설치기는 웹 요청 컨텍스트에서 돌므로 산출물은 웹 계정 소유가 되어 이후 재게시와 자연 정합한다. 실패해도 설치는 완료되고 첫 방문의 자가 치유가 재시도한다.
|
||||
- **sudo 코어 업데이트 시**: 업데이트가 root 로 실행되면 종료 시점(소유권 복원 `app.update.restore_ownership` **이후**)에 도는 terminating 게시가 root 소유 산출물을 만들 수 있다. 산출물은 게시 트리만이 아니다 — 게시는 캐시 락(`storage/framework/cache/data/xx` 샤드 디렉토리 신설)과 병합 번들 빌드(`storage/app/ext-bundles`)까지 **간접적으로 쓴다**. root 소유 캐시 샤드가 남으면 이후 웹 프로세스의 캐시 쓰기가 그 샤드에 해시되는 순간 Permission denied 로 죽어 전면 500 이 된다(7.0.10 업그레이드 실사례). 따라서 **root 프로세스에서는 terminating 자동 게시를 예약하지 않고** 다음 웹 렌더의 자가 치유(웹 계정)에 위임한다. 명시적 `ext-static:publish` 는 root 로도 실행 가능하며 게시 트리(`public/build/ext`)는 부모 소유권 상속으로 정상화되지만, storage 측 간접 산출물의 소유권은 운영자 책임이다 — sudo 로 수동 게시했다면 `storage`/`bootstrap/cache` 의 그룹 쓰기(`chgrp -R {웹그룹}` + `chmod -R g+w`)를 확인한다.
|
||||
- **sudo 코어 업데이트 시**: 업데이트가 root 로 실행되면 게시 산출물이 root 소유가 될 수 있다. 산출물은 게시 트리만이 아니다 — 게시는 캐시 락(`storage/framework/cache/data/xx` 샤드 디렉토리 신설)과 병합 번들 빌드(`storage/app/ext-bundles`)까지 **간접적으로 쓴다**. root 소유 캐시 샤드가 남으면 이후 웹 프로세스의 캐시 쓰기가 그 샤드에 해시되는 순간 Permission denied 로 죽어 전면 500 이 된다(7.0.10 업그레이드 실사례). 따라서 **root 프로세스에서는 terminating 자동 게시를 예약하지 않고** 다음 웹 렌더의 자가 치유(웹 계정)에 위임한다. 명시적 `ext-static:publish` 도 **웹 계정으로 실행한다**(`sudo -u {웹계정} php artisan ext-static:publish`) — root 로 실행하면 명령이 경고와 함께 웹 계정 실행 형태를 안내하고 계속 진행한다(중단하지는 않는다). 캐시 샤드 소유권은 이 명령에만 있는 문제가 아니라 캐시를 건드리는 **모든 artisan 명령에 공통**이므로 INSTALL.md 「명령줄·cron 실행 계정」 규율을 따른다. `ext-static:status` 도 root 로 실행 중이면 같은 경고를 낸다.
|
||||
- **코어 업데이트의 orphan 정리**: 게시본은 릴리즈 소스에 없는 로컬 파생물이므로 `app.update.excludes` 에 `build/ext` 로 등록되어 있다 — `--prune` 업데이트가 orphan 으로 삭제하지 않고, 백업 대상에서도 제외된다.
|
||||
|
||||
### CLI 계정과 웹 계정이 다를 때
|
||||
@@ -178,7 +212,7 @@ public/build/ext/{cache_version}/
|
||||
php artisan ext-static:status
|
||||
```
|
||||
|
||||
현재 버전 / 게시 여부 / manifest 파일 수 / 트리 쓰기 가능성 / 최근 실패 마커(사유별 조치 힌트 포함) / 잔존 버전 목록을 출력한다. **이상이 있으면 비-0 으로 종료**한다 — 이는 "커맨드 실행 실패" 가 아니라 조치할 대상이 있다는 신호다.
|
||||
실행 계정 / 현재 버전 / 게시 여부 / manifest 파일 수 / 트리 쓰기 가능성 / 최근 실패 마커(사유별 조치 힌트 포함) / 잔존 버전 목록을 출력한다. **이상이 있으면 비-0 으로 종료**한다 — 이는 "커맨드 실행 실패" 가 아니라 조치할 대상이 있다는 신호다. root 로 실행 중이면 웹 계정 실행 형태(`sudo -u {웹계정} php artisan ext-static:publish`)를 함께 안내한다. 같은 판정을 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드에서도 볼 수 있다(§5-1).
|
||||
|
||||
권한 문제로 게시가 계속 실패하는 환경(공유 호스팅 등)에서는 `G7_STATIC_CACHE=false` 로 기능을 끄면 경고 로그도 남지 않는다.
|
||||
|
||||
@@ -186,6 +220,8 @@ php artisan ext-static:status
|
||||
|
||||
게시물은 **로컬 디스크** 파생물이다 (확장 병합 번들 캐시와 동일한 제약). 다중 웹서버 스케일아웃 구성에서는 서버마다 게시가 필요하다 — terminating 트리거는 요청을 받은 서버에서만 실행되므로, 나머지 서버는 각자의 blade 자가 치유가 첫 렌더에서 보충한다. 공유 스토리지에 `public/build/ext` 를 올리는 구성은 rename 원자성이 보장되는 파일시스템에서만 사용한다.
|
||||
|
||||
custom 파일 변경 서명은 **서버(호스트명)별로** 기억한다. 캐시 저장소를 여러 서버가 공유하면 같은 파일이라도 서버마다 배포 시각(수정 시각)이 달라, 한 자리를 서버들이 번갈아 덮어쓰며 요청마다 "변경" 으로 읽혀 전체 재게시가 왕복한다. 서버별로 기억하면 각 서버는 자기 파일만 대조한다.
|
||||
|
||||
## 8. nginx 권장 설정 (선택)
|
||||
|
||||
버전 디렉토리라 내용이 불변이므로, 서버 기본 재검증(ETag/Last-Modified)만으로도 충분하다. 다만 **압축은 서버 몫이다** — 정적 서빙은 Laravel 의 응답 압축(GzipEncodeResponse)을 우회하므로, 서버에 gzip 설정이 없으면 종전 API 대비 전송량이 회귀한다 (실측: 병합 lang JSON 약 525KB 비압축). 권장:
|
||||
|
||||
@@ -208,6 +208,21 @@ php artisan hooks:clear # 캐시 삭제 (삭제 후
|
||||
|
||||
> 훅 캐시는 확장 install/update 및 코어 업데이트(`clearAllCaches` → `extension:update-autoload`) 시 자동 재생성됩니다. 코어 리스너 코드 배포 시에만 `hooks:cache` 수동 실행. 상세: [extension/hooks.md "정적 훅 매핑 캐시"](extension/hooks.md).
|
||||
|
||||
### 부트스트랩 리소스 정적 게시
|
||||
|
||||
```bash
|
||||
# 상태 점검 — 이상 발견 시 비-0 종료 (실행 계정·버전·게시 여부·실패 마커·잔존 버전)
|
||||
php artisan ext-static:status
|
||||
|
||||
# 수동 게시 (웹 계정으로 — root 로 실행하면 캐시 폴더가 root 소유가 되어 이후 웹 요청이 500 을 낼 수 있다)
|
||||
sudo -u www-data php artisan ext-static:publish [--force]
|
||||
|
||||
# 구버전 게시 디렉토리 정리 (현재 + 직전 1개 보존, 스케줄 일 1회 자동)
|
||||
php artisan ext-static:cleanup
|
||||
```
|
||||
|
||||
> 관리자 화면에서는 환경설정 > 일반 「초기 화면 정적 파일」 카드에서 같은 상태를 보고 [지금 다시 만들기] 로 즉시 재게시할 수 있습니다. 상세: [backend/static-asset-publishing.md](backend/static-asset-publishing.md).
|
||||
|
||||
### 단발성 결함 보정 (hotfix)
|
||||
|
||||
`hotfix:*` prefix 는 특정 버전의 결함 회복을 위해 신설되는 단발성 도구를 위한 표준 prefix 다. `core:*` (영구 운영 도구) 와 명확히 구분되며 dev-dashboard 자동 노출 면제 대상이다.
|
||||
|
||||
@@ -776,6 +776,8 @@ CSS 는 나중에 온 규칙이 이긴다. 운영자가 덧붙인 스타일이
|
||||
|
||||
이 게시가 필요한 이유는 성능이 아니라 **상대 경로**다. API 경로에서는 CSS 내부 `url('./font.woff2')` 가 해석되지 않는다(기준 URL 이 자산 디렉토리가 아니다). 정적 확장자 URL 은 public 아래 실제 파일일 때만 200 이 되므로, 게시본만이 상대 참조를 성립시킨다.
|
||||
|
||||
변경 감지 범위는 게시 범위와 같다 — `custom/` 아래 **하위 디렉토리 포함** 허용 확장자 전부(CSS·JS·글꼴·이미지)의 경로·수정 시각·크기를 본다. CSS 를 그대로 두고 참조 대상 글꼴만 교체해도 감지된다. 페이지에 실리는 로드 목록(최상위 CSS·JS)은 별개 규약이며 바뀌지 않았다. 서명은 서버(호스트명)별로 기억하므로 다중 서버가 캐시를 공유해도 서버 간 수정 시각 차이로 재게시가 왕복하지 않는다.
|
||||
|
||||
**모듈·플러그인**도 같다. 다만 게시 대상은 **활성** 확장의 `custom/` 뿐이다 — 자산 서빙이 활성 확장에만 응답하므로 비활성 확장의 파일을 게시해 봐야 아무도 참조하지 않는 사본이 쌓인다. 확장의 빌드 산출물은 개별 파일이 아니라 병합 번들로 게시되므로, `custom/` 이 그 확장에서 유일한 개별 게시 대상이다.
|
||||
|
||||
### 확장하기
|
||||
|
||||
@@ -338,10 +338,12 @@ Composer 설치 방식을 선택할 때만 필요하다.
|
||||
| WebSocket | `php artisan reverb:start` | Supervisor 등 |
|
||||
|
||||
```bash
|
||||
# cron 예시 (스케줄러)
|
||||
* * * * * cd /path/to/g7 && php artisan schedule:run >> /dev/null 2>&1
|
||||
# cron 예시 (스케줄러) — 웹 서버 계정으로 실행 (root crontab 에 그대로 두면 캐시 파일이 root 소유가 된다)
|
||||
* * * * * cd /path/to/g7 && sudo -u www-data php artisan schedule:run >> /dev/null 2>&1
|
||||
```
|
||||
|
||||
`php artisan` 명령과 cron 은 웹 서버 실행 계정으로 실행한다 — 캐시·병합 번들·임시 파일이 다른 계정 소유로 남으면 이후 웹 요청이 `Permission denied` 로 실패한다. 예외는 소유권 복원이 내장된 `core:update` 만이다. 상세: [INSTALL.md](../INSTALL.md) 「명령줄·cron 실행 계정」.
|
||||
|
||||
---
|
||||
|
||||
## 7. 지원 브라우저
|
||||
|
||||
Reference in new issue
Block a user