Files
HeuJung cfc2caba1b feat(ckeditor5,board,ecommerce,page,core,admin_basic): 미참조 에디터 업로드 이미지 정리 신설 + 업로드 고아 동종 결함 7건 전수 해소
공개 (@Tuwasduliebst 제보) 대응.

- ckeditor5: 미참조 이미지 정리 엔진 신설 — 참조 판정(hash + 파일명 2토큰,
 filter_reference_sources 훅으로 확장 콘텐츠 등록) + 정리 커맨드(기본 꺼짐 옵트인,
 3중 게이트) + 업로드 관리 화면(권한 분리·참조 배지·개별/선택 삭제) + 기설치본
 설정 백필 upgrade step
- board: 임시 첨부 GC(상시) + 소프트삭제 첨부 영구 정리(옵트인) + 설정 UI,
 저장 payload 누락(F4)·빈 temp 디렉토리 잔존(F5) 수정
- ecommerce: 임시 상품 이미지 GC + 카테고리 삭제 시 이미지 파일 미삭제(S4) 수정
- page: 임시 첨부 GC
- 코어: 고아 첨부 정리 커맨드(옵트인, site_logo 보호) + 사이트 로고 교체 시
 구 로고 재편입 차단(S6) + DB 백업 미구현 501 정직화(B3) + 플러그인 관리자
 메뉴 설치·업데이트 자동 동기화(F1·F7)
- 동반: 플러그인 설정 검증 오류 필드명 라벨화(F3), Toggle 노드 type 정정
 수평 전개(marketing 1.0.2 bump·gdpr·ecommerce), ja 언어팩 4종, API 문서,
 hooks.md, 시나리오 매니페스트 8종, Playwright E2E 4종
2026-08-17 02:44:54 +09:00

375 lines
14 KiB
PHP

<?php
namespace Plugins\Sirsoft\Ckeditor5;
use App\Enums\ExtensionOwnerType;
use App\Extension\AbstractPlugin;
use App\Extension\Helpers\ExtensionMenuSyncHelper;
/**
* CKEditor 5 WYSIWYG 에디터 플러그인
*
* extension_point: "html_editor" 슬롯을 통해 기존 HtmlEditor를 교체합니다.
* 미설치 시 기존 HtmlEditor로 자동 폴백됩니다.
*/
class Plugin extends AbstractPlugin
{
/**
* 플러그인 설정 스키마 반환
*
* @return array 설정 스키마
*/
public function getSettingsSchema(): array
{
return [
'imageUpload' => [
'type' => 'boolean',
'default' => true,
'label' => [
'ko' => '이미지 업로드',
'en' => 'Image Upload',
],
'hint' => [
'ko' => '에디터에서 이미지 업로드 기능을 활성화합니다.',
'en' => 'Enable image upload functionality in the editor.',
],
'required' => false,
],
'imageMaxSizeMb' => [
'type' => 'integer',
'default' => 2,
'label' => [
'ko' => '이미지 최대 크기 (MB)',
'en' => 'Image Max Size (MB)',
],
'hint' => [
'ko' => '업로드 가능한 이미지의 최대 파일 크기입니다.',
'en' => 'Maximum file size for uploadable images.',
],
'required' => false,
],
'editorHeight' => [
'type' => 'integer',
'default' => 400,
'label' => [
'ko' => '에디터 높이 (px)',
'en' => 'Editor Height (px)',
],
'hint' => [
'ko' => '에디터 영역의 최소 높이입니다.',
'en' => 'Minimum height of the editor area.',
],
'required' => false,
],
'toolbar' => [
'type' => 'enum',
'options' => ['standard', 'minimal', 'full'],
'default' => 'standard',
'label' => [
'ko' => '툴바 유형',
'en' => 'Toolbar Type',
],
'hint' => [
'ko' => '에디터 툴바 구성을 선택합니다.',
'en' => 'Select the editor toolbar configuration.',
],
'required' => false,
],
// 선택지가 코어 카탈로그 + 플러그인 훅 등록 디스크로 동적이라 enum 불가 — string.
// 존재하지 않는 디스크 값은 resolvePublicAssetDisk() 가 스트리밍으로 안전 폴백한다.
'public_asset_disk' => [
'type' => 'string',
'max' => 100,
'default' => '',
'label' => [
'ko' => '공개 자산 디스크',
'en' => 'Public Asset Disk',
],
'hint' => [
'ko' => '이 플러그인만 다른 디스크를 쓰려면 선택합니다. 비우면 코어 공개 자산 디스크 설정을 따릅니다.',
'en' => 'Select to use a different disk for this plugin only. Leave empty to follow the core public asset disk setting.',
],
'required' => false,
],
'unusedImageCleanup' => [
'type' => 'boolean',
'default' => false,
'label' => [
'ko' => '미사용 이미지 자동 정리',
'en' => 'Auto Cleanup Unused Images',
],
'hint' => [
'ko' => '기본 꺼짐 — 운영자가 직접 켜야 동작합니다. 켜면 보존기간이 지난 미사용 이미지를 매일 삭제합니다.',
'en' => 'Off by default — the operator must turn it on. When on, unused images past the retention period are deleted daily.',
],
'required' => false,
],
'unusedImageRetentionDays' => [
'type' => 'integer',
'min' => 1,
'max' => 3650,
'default' => 30,
'label' => [
'ko' => '미사용 이미지 보존기간 (일)',
'en' => 'Unused Image Retention (days)',
],
'hint' => [
'ko' => '업로드 후 이 기간이 지난 미사용 이미지만 정리 대상이 됩니다. (1 ~ 3650 일)',
'en' => 'Only unused images older than this period are eligible for cleanup. (1 ~ 3650 days)',
],
'required' => false,
],
];
}
/**
* 플러그인이 제공하는 훅 정보 반환
*
* @return array 훅 정의 배열 (action 2 + filter 2)
*/
public function getHooks(): array
{
return [
[
'name' => 'sirsoft-ckeditor5.image.before_upload',
'type' => 'action',
'description' => [
'ko' => '에디터 이미지 업로드 직전 발화 (본인인증·쿼터 등 확장 지점)',
'en' => 'Fired right before an editor image upload (identity verification, quota, etc.)',
],
'parameters' => [
'file' => 'UploadedFile - 업로드된 파일',
'uploadedBy' => 'int|null - 업로드 사용자 ID',
],
],
[
'name' => 'sirsoft-ckeditor5.image.after_upload',
'type' => 'action',
'description' => [
'ko' => '에디터 이미지 업로드 기록 생성 후 발화',
'en' => 'Fired after the editor image upload record is created',
],
'parameters' => [
'record' => 'Model - Ckeditor5ImageUpload',
],
],
[
'name' => 'sirsoft-ckeditor5.image.filter_upload_file',
'type' => 'filter',
'description' => [
'ko' => '업로드 파일 변형 지점 (압축·리사이즈 등)',
'en' => 'Transform the uploaded file (compression, resizing, etc.)',
],
'parameters' => [
'file' => 'UploadedFile - 업로드된 파일',
],
],
[
'name' => 'sirsoft-ckeditor5.image.filter_reference_sources',
'type' => 'filter',
'description' => [
'ko' => '에디터 이미지 참조 스캔 대상 테이블/컬럼 목록에 확장 콘텐츠를 추가',
'en' => 'Append extension content tables/columns to the editor image reference scan sources',
],
'parameters' => [
'sources' => 'array - list<array{table: string, columns: list<string>}>',
],
],
];
}
/**
* 플러그인 권한 목록 반환 (계층 구조)
*
* 업로드 이미지 관리 화면(조회/삭제)의 접근을 분리한다 — 조회는 감사·현황 파악,
* 삭제는 파일을 실제로 파기하므로 별도 권한으로 둔다.
*
* @return array 권한 정의 배열
*/
public function getPermissions(): array
{
return [
'name' => [
'ko' => 'CKEditor 5 WYSIWYG 에디터',
'en' => 'CKEditor 5 WYSIWYG Editor',
],
'description' => [
'ko' => 'CKEditor5 플러그인이 제공하는 권한',
'en' => 'Permissions provided by the CKEditor5 plugin',
],
'categories' => [
[
'identifier' => 'uploads',
'name' => ['ko' => '에디터 업로드 이미지', 'en' => 'Editor Uploads'],
'description' => [
'ko' => '에디터로 업로드된 이미지의 조회·삭제 권한',
'en' => 'View and delete permissions for images uploaded via the editor',
],
'permissions' => [
[
'action' => 'read',
'name' => ['ko' => '업로드 이미지 조회', 'en' => 'View Uploads'],
'description' => [
'ko' => '에디터 업로드 이미지 목록·참조 상태 조회',
'en' => 'View the editor upload list and reference status',
],
'type' => 'admin',
'roles' => ['admin'],
],
[
'action' => 'delete',
'name' => ['ko' => '업로드 이미지 삭제', 'en' => 'Delete Uploads'],
'description' => [
'ko' => '에디터 업로드 이미지의 파일·기록 삭제',
'en' => 'Delete editor upload files and records',
],
'type' => 'admin',
'roles' => ['admin'],
],
],
],
],
];
}
/**
* 관리자 메뉴 정의
*
* 코어 PluginManager 가 설치·업데이트 공통 경로에서 이 선언을 동기화한다.
* activate() 의 직접 동기화는 그 경로를 타지 않는 활성화 단독 호출을 위한 것이며,
* 동기화는 upsert 라 두 경로가 겹쳐도 메뉴가 중복 생성되지 않는다.
*
* @return array<int, array<string, mixed>>
*/
public function getAdminMenus(): array
{
return [
[
'name' => ['ko' => '에디터 업로드 이미지', 'en' => 'Editor Uploads'],
'slug' => 'sirsoft-ckeditor5-uploads',
'url' => '/admin/plugins/sirsoft-ckeditor5/uploads',
'icon' => 'fas fa-images',
'order' => 50,
],
];
}
/**
* 플러그인 활성화 — 관리자 메뉴 자동 등록.
*
* @return bool 활성화 성공 여부
*/
public function activate(): bool
{
$helper = app(ExtensionMenuSyncHelper::class);
foreach ($this->getAdminMenus() as $menuData) {
$helper->syncMenuRecursive(
$menuData,
ExtensionOwnerType::Plugin,
$this->getIdentifier(),
);
}
return true;
}
/**
* 플러그인 비활성화 — 관리자 메뉴 일괄 제거.
*
* @return bool 비활성화 성공 여부
*/
public function deactivate(): bool
{
app(ExtensionMenuSyncHelper::class)->cleanupStaleMenus(
ExtensionOwnerType::Plugin,
$this->getIdentifier(),
currentSlugs: [],
);
return true;
}
/**
* 플러그인 제거 — 메뉴 잔존 안전망 (정상 흐름은 deactivate 가 먼저 처리).
*
* @return bool 제거 성공 여부
*/
public function uninstall(): bool
{
$this->deactivate();
return true;
}
/**
* 플러그인 스케줄 목록 반환
*
* 미참조 이미지 정리는 사용자 파일을 지우므로 기본 꺼짐(옵트인)이다.
* enabled_config 게이트는 설정 조회 실패 시 true 로 폴백하므로, 커맨드가
* `--scheduled` 에서 같은 설정을 false 폴백으로 재확인해 자동 삭제를 차단한다.
*
* @return array<int, array<string, string>> 스케줄 정의 목록
*/
public function getSchedules(): array
{
return [
[
'command' => 'sirsoft-ckeditor5:prune-unused-images --scheduled',
'schedule' => 'daily',
'description' => '미참조 에디터 업로드 이미지 정리',
'enabled_config' => 'sirsoft-ckeditor5.unusedImageCleanup',
],
];
}
/**
* 완전 공개 자산 스토리지 카테고리 목록
*
* 이 카테고리들만 공개 자산 디스크 설정을 따른다. 권한 검사가 걸린 자산은
* 직접 URL 이 권한을 우회하므로 포함하지 않는다. 에디터가 이미지 외의
* 공개 자산 업로드를 지원하게 되면 여기에만 카테고리를 추가하면 된다.
*
* @var list<string>
*/
private const PUBLIC_ASSET_CATEGORIES = ['images'];
/**
* 카테고리별 스토리지 디스크 이름 반환
*
* 완전 공개 자산 카테고리만 공개 자산 디스크(플러그인 설정 public_asset_disk >
* 코어 전역 core.storage.public_asset_disk)를 따르고, 설정 등 나머지 카테고리는
* 기본 디스크를 유지합니다. 미설정/고아 디스크는 기본 디스크로 폴백해 기존
* 스트리밍 동작을 보존합니다.
*
* 플러그인 설정 조회는 공개 자산 카테고리에서만 수행합니다 — 'settings' 카테고리에서
* 조회하면 설정 로드와 재귀 고리가 생깁니다 (AbstractPlugin 주석 참조).
*
* @param string $category 카테고리
* @return string 디스크 이름
*/
public function getStorageDiskFor(string $category): string
{
if (! in_array($category, self::PUBLIC_ASSET_CATEGORIES, true)) {
return $this->getStorageDisk();
}
$override = plugin_setting('sirsoft-ckeditor5', 'public_asset_disk', '');
return $this->resolvePublicAssetDisk(is_string($override) ? $override : '')
?? $this->getStorageDisk();
}
/**
* 플러그인이 관리하는 동적 테이블 목록 반환
*
* @return array 테이블명 배열
*/
public function getDynamicTables(): array
{
return [
'ckeditor5_image_uploads',
];
}
}