v7.0.0 release

This commit is contained in:
HeuJung committed 2026-07-01 10:30:32 +09:00
1 parent 0e7fa04b03
commit c4ea9a6cd1
3582 files changed
+626189 -160151

No files matched your search

@@ -0,0 +1,43 @@
<?php
namespace Database\Factories;
use App\Models\LayoutExtension;
use App\Models\TemplateLayoutExtensionVersion;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<TemplateLayoutExtensionVersion>
*/
class TemplateLayoutExtensionVersionFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<TemplateLayoutExtensionVersion>
*/
protected $model = TemplateLayoutExtensionVersion::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'extension_id' => LayoutExtension::factory(),
'version' => fake()->unique()->numberBetween(1, 1000),
'content' => [
'extension_point' => fake()->word(),
'components' => [],
],
'changes_summary' => [
'added' => [],
'removed' => [],
'modified' => [],
'char_diff' => fake()->numberBetween(-500, 500),
],
];
}
}
@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('template_layout_extension_versions', function (Blueprint $table) {
$table->id()->comment('버전 ID');
$table->foreignId('extension_id')->constrained('template_layout_extensions')->cascadeOnDelete()->comment('레이아웃 확장 ID');
$table->unsignedInteger('version')->comment('버전 번호 (자동 증가)');
$table->longText('content')->comment('확장 정의 JSON 스냅샷');
$table->text('changes_summary')->nullable()->comment('변경 요약 JSON: {"added": 3, "removed": 2, "is_restored": false, "restored_from": null}');
$table->unsignedBigInteger('created_by')->nullable()->comment('저장자 ID');
$table->timestamp('created_at')->nullable()->comment('생성 일시');
$table->unique(['extension_id', 'version'], 'uk_ext_layout_version');
$table->index(['extension_id', 'created_at'], 'idx_ext_layout_versions');
});
if (DB::getDriverName() == 'mysql') {
Schema::table('template_layout_extension_versions', function (Blueprint $table) {
$table->comment('템플릿 레이아웃 확장 버전 이력');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('template_layout_extension_versions');
}
};
@@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('template_layout_extensions', function (Blueprint $table) {
if (! Schema::hasColumn('template_layout_extensions', 'original_content_hash')) {
$table->string('original_content_hash', 64)
->nullable()
->after('content')
->comment('원본 확장 콘텐츠 SHA-256 해시 (수정 감지용)');
}
if (! Schema::hasColumn('template_layout_extensions', 'original_content_size')) {
$table->unsignedInteger('original_content_size')
->nullable()
->after('original_content_hash')
->comment('원본 확장 콘텐츠 바이트 크기 (변화량 표시용)');
}
});
// 기존 데이터 hash 백필 (현재 content = 원본으로 간주)
DB::table('template_layout_extensions')
->whereNull('original_content_hash')
->orderBy('id')
->chunk(100, function ($extensions) {
foreach ($extensions as $extension) {
$content = json_decode($extension->content, true);
$normalized = json_encode($content, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$hash = hash('sha256', $normalized);
$size = strlen($normalized);
DB::table('template_layout_extensions')
->where('id', $extension->id)
->update([
'original_content_hash' => $hash,
'original_content_size' => $size,
]);
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('template_layout_extensions', function (Blueprint $table) {
$table->dropColumn(['original_content_hash', 'original_content_size']);
});
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('template_layout_previews', function (Blueprint $table) {
if (! Schema::hasColumn('template_layout_previews', 'preview_type')) {
$table->string('preview_type', 20)
->default('layout')
->after('layout_name')
->comment('미리보기 종류 (layout: 일반 레이아웃, extension: 레이아웃 확장)');
}
if (! Schema::hasColumn('template_layout_previews', 'extension_id')) {
$table->unsignedBigInteger('extension_id')
->nullable()
->after('preview_type')
->comment('레이아웃 확장 ID (preview_type=extension 일 때)');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('template_layout_previews', function (Blueprint $table) {
$table->dropColumn(['preview_type', 'extension_id']);
});
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('template_layouts', function (Blueprint $table) {
if (! Schema::hasColumn('template_layouts', 'lock_version')) {
$table->unsignedBigInteger('lock_version')->default(0)->after('updated_by')->comment('낙관적 잠금 버전');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('template_layouts', function (Blueprint $table) {
if (Schema::hasColumn('template_layouts', 'lock_version')) {
$table->dropColumn('lock_version');
}
});
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('template_layout_extensions', function (Blueprint $table) {
if (! Schema::hasColumn('template_layout_extensions', 'lock_version')) {
$table->unsignedBigInteger('lock_version')->default(0)->after('is_active')->comment('낙관적 잠금 버전');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('template_layout_extensions', function (Blueprint $table) {
if (Schema::hasColumn('template_layout_extensions', 'lock_version')) {
$table->dropColumn('lock_version');
}
});
}
};
@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 레이아웃 편집 중 업로드되는 파일(배경 이미지 등)을 관리하는 테이블.
* 파일 저장은 코어 StorageInterface 를 통해 수행하고, 저장 위치(disk/path)와
* 메타데이터를 본 테이블에 기록한다.
*/
public function up(): void
{
Schema::create('template_layout_attachments', function (Blueprint $table) {
$table->id()->comment('첨부 파일 ID');
$table->foreignId('template_id')
->constrained('templates')
->cascadeOnDelete()
->comment('소속 템플릿 ID');
$table->string('layout_name', 150)->nullable()->comment('사용 출처 레이아웃 이름');
$table->string('disk', 50)->comment('스토리지 디스크 이름');
$table->string('path', 500)->comment('스토리지 내 파일 경로');
$table->string('original_name', 255)->comment('업로드 원본 파일명');
$table->string('mime_type', 100)->comment('MIME 타입');
$table->unsignedBigInteger('size')->comment('파일 크기(바이트)');
$table->foreignId('created_by')
->nullable()
->constrained('users')
->nullOnDelete()
->comment('업로더 사용자 ID');
$table->timestamps();
$table->index(['template_id', 'layout_name'], 'index_template_layout');
$table->index(['disk', 'path'], 'index_disk_path');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasTable('template_layout_attachments')) {
Schema::dropIfExists('template_layout_attachments');
}
}
};
@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('template_custom_translations', function (Blueprint $table) {
$table->id()->comment('커스텀 다국어 키 ID');
$table->foreignId('template_id')
->constrained('templates')
->cascadeOnDelete()
->comment('소속 템플릿 ID');
$table->string('layout_name', 150)->nullable()->comment('생성 출처 레이아웃 이름');
$table->string('translation_key', 255)->comment('다국어 키 ($t: 참조 경로)');
$table->json('values')->comment('로케일별 번역 값');
$table->json('user_overrides')->nullable()->comment('사용자 수정 보존 추적');
$table->enum('status', ['active', 'orphaned'])->default('active')->comment('상태 (active: 활성, orphaned: 고아)');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('생성자');
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete()->comment('수정자');
$table->timestamps();
$table->unique(['template_id', 'translation_key'], 'template_custom_translations_template_key_unique');
$table->index(['template_id', 'layout_name'], 'template_custom_translations_template_layout_index');
$table->index('status', 'template_custom_translations_status_index');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('template_custom_translations');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('template_custom_translations', function (Blueprint $table) {
if (! Schema::hasColumn('template_custom_translations', 'lock_version')) {
$table->unsignedBigInteger('lock_version')->default(0)->after('updated_by')->comment('낙관적 잠금 버전');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('template_custom_translations', function (Blueprint $table) {
if (Schema::hasColumn('template_custom_translations', 'lock_version')) {
$table->dropColumn('lock_version');
}
});
}
};