Merge pull request from gnuboard:HeuJung/issue555

HeuJung/issue555
This commit is contained in:
정정홍
2026-08-12 15:24:30 +09:00
committed by GitHub
18 changed files with 1279 additions and 6 deletions
+4
View File
@@ -6,6 +6,10 @@
## [7.0.7] - 2026-08-11
### Added
- 레이아웃 편집기 첨부 파일 업로드에 업로드 전/후 액션 훅과 파일 가공 필터 훅 제공 — 확장에서 다른 업로드 경로와 동일하게 개입할 수 있습니다.
### Fixed
- 확인 질문이 있는 Artisan 명령(확장 설치·삭제·업데이트, 코어 업데이트, 검색 인덱스 재생성 등)을 터미널이 아닌 곳에서 실행하면 아무 반응 없이 멈춰 있던 문제를 수정했습니다. 예약 작업·배포 스크립트·CI 처럼 사람이 답할 수 없는 환경에서는 질문이 화면에 나오지도 않은 채 응답을 기다렸기 때문에, 명령이 실패한 것도 아니고 진행 중인 것도 아닌 상태로 남아 "명령이 몹시 느리다" 로만 보였습니다. 이제 그런 환경에서는 질문을 건너뛰고 기본 동작을 따릅니다 — 삭제·업데이트처럼 되돌리기 어려운 명령의 기본 동작은 "중단" 이므로, 확인 없이 수행되는 일은 없습니다. 그대로 진행하려면 종전처럼 `--no-interaction` 이나 `--force` 를 붙이면 됩니다.
+1
View File
@@ -527,6 +527,7 @@ cp .env.example .env
<a href="https://github.com/devrhee16" title="devrhee16"><img src="https://github.com/devrhee16.png" width="48" alt="devrhee16"></a>
<a href="https://github.com/GyusoonKim" title="GyusoonKim"><img src="https://github.com/GyusoonKim.png" width="48" alt="GyusoonKim"></a>
<a href="https://github.com/Lastorder-DC" title="Lastorder-DC"><img src="https://github.com/Lastorder-DC.png" width="48" alt="Lastorder-DC"></a>
<a href="https://github.com/lyg-kaban" title="lyg-kaban"><img src="https://github.com/lyg-kaban.png" width="48" alt="lyg-kaban"></a>
<a href="https://github.com/miles44229" title="miles44229"><img src="https://github.com/miles44229.png" width="48" alt="miles44229"></a>
<a href="https://github.com/minyho" title="minyho"><img src="https://github.com/minyho.png" width="48" alt="minyho"></a>
</p>
+1
View File
@@ -543,6 +543,7 @@ Thanks to everyone who reported an issue or suggested a feature that shipped —
<a href="https://github.com/devrhee16" title="devrhee16"><img src="https://github.com/devrhee16.png" width="48" alt="devrhee16"></a>
<a href="https://github.com/GyusoonKim" title="GyusoonKim"><img src="https://github.com/GyusoonKim.png" width="48" alt="GyusoonKim"></a>
<a href="https://github.com/Lastorder-DC" title="Lastorder-DC"><img src="https://github.com/Lastorder-DC.png" width="48" alt="Lastorder-DC"></a>
<a href="https://github.com/lyg-kaban" title="lyg-kaban"><img src="https://github.com/lyg-kaban.png" width="48" alt="lyg-kaban"></a>
<a href="https://github.com/miles44229" title="miles44229"><img src="https://github.com/miles44229.png" width="48" alt="miles44229"></a>
<a href="https://github.com/minyho" title="minyho"><img src="https://github.com/minyho.png" width="48" alt="minyho"></a>
</p>
@@ -5,6 +5,7 @@ namespace App\Services;
use App\Contracts\Extension\StorageInterface;
use App\Contracts\Repositories\TemplateLayoutAttachmentRepositoryInterface;
use App\Contracts\Repositories\TemplateRepositoryInterface;
use App\Extension\HookManager;
use App\Models\TemplateLayoutAttachment;
use App\Support\ImageResizer;
use Illuminate\Database\Eloquent\Collection;
@@ -52,6 +53,12 @@ class TemplateLayoutAttachmentService
return ['success' => false, 'attachment' => null, 'url' => null, 'error' => 'template_not_found'];
}
// 훅: 업로드 전 (확장 지점)
HookManager::doAction('core.template_layout_attachment.before_upload', $file, $templateIdentifier, $layoutName);
// 필터 훅 - 파일 데이터 변형 (압축, 리사이즈 등 확장 포인트)
$file = HookManager::applyFilters('core.template_layout_attachment.filter_upload_file', $file);
// 저장 경로 — 템플릿 식별자/날짜별 디렉토리 + UUID 파일명 (충돌 회피)
$storedFilename = Str::uuid().'.'.$file->getClientOriginalExtension();
$relativePath = "{$templateIdentifier}/".date('Y/m/d')."/{$storedFilename}";
@@ -87,6 +94,8 @@ class TemplateLayoutAttachmentService
'path' => $relativePath,
]);
HookManager::doAction('core.template_layout_attachment.after_upload', $attachment);
return [
'success' => true,
'attachment' => $attachment,
+17
View File
@@ -121,6 +121,23 @@ core.attachment.download
core.attachment.update
core.attachment.delete
# 업로드 트라이어드 — 사용자 첨부 업로드 지점의 표준 3훅 패턴
# before_upload(액션) → filter_upload_file(필터: UploadedFile 을 받아 변형본을 반환.
# 저장 파일명·MIME·크기가 모두 반환 파일 기준이 된다) → after_upload(액션)
# 포맷 변환(예: jpg → webp) 시에는 바이트만 제자리 덮어쓰지 말고, 변환된 임시 파일 경로와
# 새 원본 파일명(xxx.webp)으로 UploadedFile 을 재구성해 반환할 것 — 일부 소비처는 저장
# 확장자를 원본 파일명에서 얻으므로, 이름을 갱신하지 않으면 확장자와 내용이 어긋난다.
core.attachment.filter_upload_file # 코어 첨부 (아바타 포함)
core.template_layout_attachment.before_upload # 레이아웃 편집기 첨부 (7.0.7)
core.template_layout_attachment.filter_upload_file # 레이아웃 편집기 첨부 (7.0.7)
core.template_layout_attachment.after_upload # 레이아웃 편집기 첨부 (7.0.7)
sirsoft-board.attachment.filter_upload_file # 게시판 첨부
sirsoft-page.attachment.filter_upload_file # 페이지 첨부
sirsoft-ecommerce.product-image.filter_upload_file # 상품 이미지
sirsoft-ecommerce.category-image.filter_upload_file # 카테고리 이미지
sirsoft-ecommerce.review-image.filter_upload_file # 리뷰 이미지 (공개 #96)
sirsoft-ckeditor5.image.filter_upload_file # 에디터 이미지
# FormRequest Validation Rules 훅 (Filter)
core.user.create_validation_rules
core.user.update_validation_rules
@@ -6,6 +6,10 @@
## [1.1.1] - 2026-08-11
### Added
- 리뷰 이미지 업로드에 파일 가공 필터 훅 제공 — 확장에서 상품·카테고리 이미지와 동일하게 리뷰 이미지도 업로드 시점에 변환(압축·리사이즈·포맷 변경)할 수 있습니다. (#96 @lyg-kaban 님께서 건의해주셨습니다.)
### Fixed
- 언어/통화 설정에서 기본 제공 통화(달러·엔·위안·유로)를 삭제해도 저장 직후 다시 나타나던 문제를 수정했습니다. 삭제한 통화는 설정 화면과 쇼핑몰 화면 양쪽에서 유지되며, 통화 추가로 다시 등록하면 복원됩니다. 사용 중지된 통화를 표시 통화로 갖고 있던 구매자의 주문은 기본 통화로 안전하게 진행됩니다. (#91 @koojunho 님께서 제보해주셨습니다.)
@@ -58,6 +58,9 @@ class ProductReviewImageService
HookManager::doAction('sirsoft-ecommerce.review-image.before_upload', $file, $review);
// 필터 훅 - 파일 데이터 변형 (압축, 리사이즈 등 확장 포인트)
$file = HookManager::applyFilters('sirsoft-ecommerce.review-image.filter_upload_file', $file);
$storedFilename = Str::uuid().'.'.$file->getClientOriginalExtension();
$path = "reviews/{$review->id}/{$storedFilename}";
@@ -3,6 +3,7 @@
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services;
use App\Contracts\Extension\StorageInterface;
use App\Extension\HookManager;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
@@ -177,6 +178,51 @@ class ProductReviewImageServiceTest extends ModuleTestCase
$this->assertFalse($image->is_thumbnail);
}
#[Test]
public function test_upload_fires_hooks(): void
{
// Arrange
$beforeUploadFired = false;
$afterUploadFired = false;
$filterApplied = false;
HookManager::addAction('sirsoft-ecommerce.review-image.before_upload', function () use (&$beforeUploadFired) {
$beforeUploadFired = true;
});
HookManager::addFilter('sirsoft-ecommerce.review-image.filter_upload_file', function ($file) use (&$filterApplied) {
$filterApplied = true;
// 반환 파일이 실제로 소비되는지 검증하기 위해 다른 이름/확장자의 파일을 반환
return UploadedFile::fake()->image('filtered.webp');
});
HookManager::addAction('sirsoft-ecommerce.review-image.after_upload', function () use (&$afterUploadFired) {
$afterUploadFired = true;
});
$file = UploadedFile::fake()->image('original.jpg');
$this->settingsService->shouldReceive('getSetting')->andReturn(5);
$this->storage->shouldReceive('put')->andReturn(true);
$this->storage->shouldReceive('getDisk')->andReturn('local');
// Act
$image = $this->service->upload($file, $this->review);
// Assert
$this->assertTrue($beforeUploadFired, 'before_upload hook should be fired');
$this->assertTrue($filterApplied, 'filter_upload_file hook should be applied');
$this->assertTrue($afterUploadFired, 'after_upload hook should be fired');
// 필터가 반환한 파일이 저장 파일명의 근거가 되어야 한다 (반환값 소비 증명)
$this->assertStringEndsWith('.webp', $image->stored_filename, 'filter return value should be consumed');
// Cleanup hooks
HookManager::clearAction('sirsoft-ecommerce.review-image.before_upload');
HookManager::clearFilter('sirsoft-ecommerce.review-image.filter_upload_file');
HookManager::clearAction('sirsoft-ecommerce.review-image.after_upload');
}
// ========================================
// delete() 테스트
// ========================================
@@ -41,6 +41,7 @@ effects:
- front_props_reflect_settings # maxFiles/maxSize 동적 바인딩
- attribute_label_not_raw_key # 음수 입력 시 한글/영문 라벨
- review_settings_save_logs_activity # 리뷰설정 저장 활동로그
- review_image_filter_upload_file_hook_applied # 업로드 경로 filter_upload_file 발화 (공개 #96, upload-filter-hook-parity.yaml)
# 설정 화면 JSON ↔ 저장 규칙 경계 정합 (#493 E10)
- non_integer_image_size_rejected_422 # step:0.5 가 오류 — 규칙은 integer
- settings_screen_bounds_match_rules # 화면 min/max/step 이 서버 경계와 동일
@@ -4,6 +4,12 @@
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
## [1.0.2] - 2026-08-12
### Added
- 에디터 이미지 업로드에 파일 가공 필터 훅 제공 — 확장에서 업로드 이미지를 저장 전에 변환할 수 있습니다.
## [1.0.1] - 2026-08-10
### Changed
@@ -2,7 +2,7 @@
"name": "plugins/sirsoft-ckeditor5",
"description": "CKEditor 5 WYSIWYG Editor Plugin for Gnuboard7 platform",
"type": "library",
"version": "1.0.1",
"version": "1.0.2",
"authors": [
{
"name": "sirsoft",
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
{
"name": "@g7/sirsoft-ckeditor5",
"version": "1.0.1",
"version": "1.0.2",
"description": "그누보드7 CKEditor 5 WYSIWYG 에디터 플러그인 프론트엔드 에셋",
"private": true,
"type": "module",
@@ -5,7 +5,7 @@
"ko": "CKEditor 5 WYSIWYG 에디터",
"en": "CKEditor 5 WYSIWYG Editor"
},
"version": "1.0.1",
"version": "1.0.2",
"license": "MIT",
"description": {
"ko": "CKEditor 5를 이용한 WYSIWYG 에디터 플러그인입니다. 플러그인 설치만으로 기존 HtmlEditor가 교체됩니다.",
@@ -38,6 +38,9 @@ class ImageUploadService
// 훅: 이미지 업로드 전 (본인인증 등 확장 지점)
HookManager::doAction('sirsoft-ckeditor5.image.before_upload', $file, $uploadedBy);
// 필터 훅 - 파일 데이터 변형 (압축, 리사이즈 등 확장 포인트)
$file = HookManager::applyFilters('sirsoft-ckeditor5.image.filter_upload_file', $file);
$extension = $file->guessExtension() ?? $file->getClientOriginalExtension();
$storedFilename = Str::uuid().'.'.$extension;
$path = date('Y/m/d').'/'.$storedFilename;
@@ -0,0 +1,91 @@
<?php
namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit\Services;
use App\Contracts\Extension\StorageInterface;
use App\Extension\HookManager;
use Illuminate\Http\UploadedFile;
use Mockery;
use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\Test;
use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload;
use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface;
use Plugins\Sirsoft\Ckeditor5\Services\ImageUploadService;
use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase;
/**
* ImageUploadService 단위 테스트
*
* 업로드 훅 트라이어드(before_upload → filter_upload_file → after_upload) 발화를 검증합니다.
* upload 자체 동작(저장/기록)은 Feature ImageUploadControllerTest 가 커버합니다.
*/
class ImageUploadServiceTest extends PluginTestCase
{
private ImageUploadService $service;
/** @var MockInterface&ImageUploadRepositoryInterface */
private $repository;
/** @var MockInterface&StorageInterface */
private $storage;
protected function setUp(): void
{
parent::setUp();
$this->repository = Mockery::mock(ImageUploadRepositoryInterface::class);
$this->storage = Mockery::mock(StorageInterface::class);
$this->service = new ImageUploadService($this->repository, $this->storage);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[Test]
public function test_upload_fires_hooks(): void
{
// Arrange
$beforeUploadFired = false;
$afterUploadFired = false;
$filterApplied = false;
HookManager::addAction('sirsoft-ckeditor5.image.before_upload', function () use (&$beforeUploadFired) {
$beforeUploadFired = true;
});
HookManager::addFilter('sirsoft-ckeditor5.image.filter_upload_file', function ($file) use (&$filterApplied) {
$filterApplied = true;
return $file;
});
HookManager::addAction('sirsoft-ckeditor5.image.after_upload', function () use (&$afterUploadFired) {
$afterUploadFired = true;
});
$file = UploadedFile::fake()->image('test.jpg');
$this->storage->shouldReceive('put')->andReturn(true);
$this->storage->shouldReceive('getDisk')->andReturn('local');
$record = new Ckeditor5ImageUpload;
$this->repository->shouldReceive('create')->andReturn($record);
// Act
$this->service->upload($file, null);
// Assert
$this->assertTrue($beforeUploadFired, 'before_upload hook should be fired');
$this->assertTrue($filterApplied, 'filter_upload_file hook should be applied');
$this->assertTrue($afterUploadFired, 'after_upload hook should be fired');
// Cleanup hooks
HookManager::clearAction('sirsoft-ckeditor5.image.before_upload');
HookManager::clearFilter('sirsoft-ckeditor5.image.filter_upload_file');
HookManager::clearAction('sirsoft-ckeditor5.image.after_upload');
}
}
@@ -3,6 +3,7 @@
namespace Tests\Feature\Api\Admin;
use App\Enums\ExtensionStatus;
use App\Extension\HookManager;
use App\Models\Permission;
use App\Models\Role;
use App\Models\Template;
@@ -103,6 +104,46 @@ class TemplateLayoutAttachmentControllerTest extends TestCase
Storage::disk($attachment->disk)->assertExists('template-layout-attachments/'.$attachment->path);
}
public function test_upload_fires_hooks(): void
{
// Arrange
$beforeUploadFired = false;
$afterUploadFired = false;
$filterApplied = false;
HookManager::addAction('core.template_layout_attachment.before_upload', function () use (&$beforeUploadFired) {
$beforeUploadFired = true;
});
HookManager::addFilter('core.template_layout_attachment.filter_upload_file', function ($file) use (&$filterApplied) {
$filterApplied = true;
return $file;
});
HookManager::addAction('core.template_layout_attachment.after_upload', function () use (&$afterUploadFired) {
$afterUploadFired = true;
});
// Act
$response = $this->withHeaders($this->authHeaders())
->postJson("/api/admin/templates/{$this->template->identifier}/layout-attachments", [
'file' => UploadedFile::fake()->image('hooked.png', 10, 10),
'layout_name' => 'home',
]);
// Assert
$response->assertStatus(200)->assertJson(['success' => true]);
$this->assertTrue($beforeUploadFired, 'before_upload hook should be fired');
$this->assertTrue($filterApplied, 'filter_upload_file hook should be applied');
$this->assertTrue($afterUploadFired, 'after_upload hook should be fired');
// Cleanup hooks
HookManager::clearAction('core.template_layout_attachment.before_upload');
HookManager::clearFilter('core.template_layout_attachment.filter_upload_file');
HookManager::clearAction('core.template_layout_attachment.after_upload');
}
public function test_index_lists_template_attachments(): void
{
TemplateLayoutAttachment::create([
@@ -0,0 +1,26 @@
# audit:allow test-scenario-coverage reason: |
# 백엔드 전용(훅 발화·필터 반환값 소비) 변경으로 PHPUnit 단위/Feature 테스트가
# 표면×구독자 축 조합을 직접 커버하고 green 이다. cross product 자동 전개 불필요.
feature: 업로드 filter_upload_file 필터 훅 대칭화 (공개 #96)
description: |
before/after 액션만 있고 filter_upload_file 이 없던 업로드 지점(리뷰 이미지·ckeditor5 이미지)과
훅 트라이어드가 전무하던 지점(템플릿 레이아웃 첨부)에 필터 훅을 대칭 제공한다.
구독자가 없으면 applyFilters 는 입력을 그대로 반환하므로 기존 동작 불변.
axes:
surface: [review_image, ckeditor5_image, template_layout_attachment]
subscriber: [none, registered]
effects:
- before_upload_action_fired
- filter_upload_file_applied # 등록된 필터가 업로드 경로에서 발화
- filter_return_value_consumed # 필터 반환 파일이 저장 파일명/메타의 근거가 됨
- after_upload_action_fired
- no_subscriber_passthrough # 구독자 0일 때 기존 동작 불변 (기존 upload 테스트가 커버)
test_files:
- modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceTest.php
- plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageUploadServiceTest.php
- tests/Feature/Api/Admin/TemplateLayoutAttachmentControllerTest.php