v7.0.0-beta.2 release
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Enums\MailSendStatus;
|
||||
use App\Models\MailSendLog;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* 메일 발송 이력 팩토리
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\MailSendLog>
|
||||
*/
|
||||
class MailSendLogFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 모델 클래스
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = MailSendLog::class;
|
||||
|
||||
/**
|
||||
* 기본 상태 정의
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'sender_email' => $this->faker->safeEmail(),
|
||||
'sender_name' => $this->faker->name(),
|
||||
'recipient_email' => $this->faker->safeEmail(),
|
||||
'recipient_name' => $this->faker->name(),
|
||||
'subject' => $this->faker->sentence(),
|
||||
'body' => '<p>'.$this->faker->paragraph().'</p>',
|
||||
'template_type' => $this->faker->randomElement(['welcome', 'password_reset', 'new_comment']),
|
||||
'extension_type' => ExtensionOwnerType::Core->value,
|
||||
'extension_identifier' => 'core',
|
||||
'source' => $this->faker->randomElement(['notification', 'test_mail', null]),
|
||||
'status' => MailSendStatus::Sent->value,
|
||||
'error_message' => null,
|
||||
'sent_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 실패 상태
|
||||
*/
|
||||
public function failed(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => MailSendStatus::Failed->value,
|
||||
'error_message' => $this->faker->sentence(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 확장 지정
|
||||
*
|
||||
* @param ExtensionOwnerType $type 확장 타입
|
||||
* @param string $identifier 확장 식별자
|
||||
* @return static
|
||||
*/
|
||||
public function forExtension(ExtensionOwnerType $type, string $identifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'extension_type' => $type->value,
|
||||
'extension_identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 템플릿 유형 지정
|
||||
*/
|
||||
public function withTemplateType(string $templateType): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'template_type' => $templateType,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\MailTemplate;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* MailTemplate 팩토리
|
||||
*
|
||||
* @extends Factory<MailTemplate>
|
||||
*/
|
||||
class MailTemplateFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* @var string 팩토리 모델 클래스
|
||||
*/
|
||||
protected $model = MailTemplate::class;
|
||||
|
||||
/**
|
||||
* 기본 상태를 정의합니다.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->faker->unique()->slug(2),
|
||||
'subject' => [
|
||||
'ko' => '[테스트] '.$this->faker->sentence(),
|
||||
'en' => '[Test] '.$this->faker->sentence(),
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<p>'.$this->faker->paragraph().'</p>',
|
||||
'en' => '<p>'.$this->faker->paragraph().'</p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_default' => true,
|
||||
'updated_by' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 비활성 상태
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn () => ['is_active' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 유형
|
||||
*
|
||||
* @param string $type 템플릿 유형
|
||||
* @return static
|
||||
*/
|
||||
public function withType(string $type): static
|
||||
{
|
||||
return $this->state(fn () => ['type' => $type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 변수 포함 본문
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withVariables(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'subject' => [
|
||||
'ko' => '[{app_name}] {name}님 환영합니다',
|
||||
'en' => '[{app_name}] Welcome {name}',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<p>{name}님, {app_name}에 가입해 주셔서 감사합니다.</p>',
|
||||
'en' => '<p>Welcome {name}, thank you for joining {app_name}.</p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\NotificationDefinition;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* NotificationDefinition 팩토리
|
||||
*
|
||||
* @extends Factory<NotificationDefinition>
|
||||
*/
|
||||
class NotificationDefinitionFactory extends Factory
|
||||
{
|
||||
protected $model = NotificationDefinition::class;
|
||||
|
||||
/**
|
||||
* 기본 상태를 정의합니다.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->faker->unique()->slug(2),
|
||||
'hook_prefix' => 'core',
|
||||
'extension_type' => 'core',
|
||||
'extension_identifier' => '',
|
||||
'name' => ['ko' => $this->faker->words(2, true), 'en' => $this->faker->words(2, true)],
|
||||
'description' => ['ko' => $this->faker->sentence(), 'en' => $this->faker->sentence()],
|
||||
'variables' => [],
|
||||
'channels' => ['database'],
|
||||
'hooks' => [],
|
||||
'is_active' => true,
|
||||
'is_default' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* 알림 정의 테이블 생성.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notification_definitions', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->string('type', 100)->unique()->comment('알림 타입 (welcome, order_confirmed 등)');
|
||||
$table->string('hook_prefix', 100)->comment('훅 접두사 (core.auth, sirsoft-ecommerce 등)');
|
||||
$table->string('extension_type', 20)->comment('확장 타입: core, module, plugin');
|
||||
$table->string('extension_identifier', 100)->comment('확장 식별자: core, sirsoft-board 등');
|
||||
$table->text('name')->comment('다국어 이름 ({"ko": "회원가입 환영", "en": "Welcome"})');
|
||||
$table->mediumText('description')->nullable()->comment('다국어 설명');
|
||||
$table->text('variables')->nullable()->comment('사용 가능 변수 메타데이터 ([{key, description}])');
|
||||
$table->text('channels')->comment('활성 채널 (["mail", "database"])');
|
||||
$table->text('hooks')->comment('트리거 훅 목록 (["core.auth.after_register"])');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 여부');
|
||||
$table->boolean('is_default')->default(true)->comment('시더 생성 여부');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 정의 테이블 삭제.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notification_definitions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 알림 템플릿 테이블 생성.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notification_templates', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->foreignId('definition_id')->constrained('notification_definitions')->cascadeOnDelete()->comment('알림 정의 ID');
|
||||
$table->string('channel', 50)->comment('채널: mail, database, fcm');
|
||||
$table->text('subject')->nullable()->comment('다국어 제목 ({"ko": "...", "en": "..."})');
|
||||
$table->mediumText('body')->comment('다국어 본문 ({"ko": "...", "en": "..."})');
|
||||
$table->boolean('is_active')->default(true)->comment('해당 채널 활성 여부');
|
||||
$table->boolean('is_default')->default(true)->comment('시더 생성 여부');
|
||||
$table->text('user_overrides')->nullable()->comment('사용자가 수정한 필드명 목록');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete()->comment('수정자');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['definition_id', 'channel']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 템플릿 테이블 삭제.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notification_templates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 사이트내 알림 테이블 생성 (Laravel 표준).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary()->comment('UUID');
|
||||
$table->string('type')->comment('알림 클래스명');
|
||||
$table->string('notifiable_type')->comment('알림 수신 모델 타입');
|
||||
$table->unsignedBigInteger('notifiable_id')->comment('알림 수신 모델 ID');
|
||||
$table->index(['notifiable_type', 'notifiable_id']);
|
||||
$table->text('data')->comment('알림 데이터 (JSON)');
|
||||
$table->timestamp('read_at')->nullable()->comment('읽음 시각');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이트내 알림 테이블 삭제.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 알림 발송 이력 테이블 생성.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notification_logs', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->string('channel', 50)->comment('채널: mail, database, fcm 등');
|
||||
$table->string('notification_type', 100)->comment('알림 타입: welcome, order_confirmed 등');
|
||||
$table->string('extension_type', 20)->default('core')->comment('확장 타입: core, module, plugin');
|
||||
$table->string('extension_identifier', 100)->default('core')->comment('확장 식별자');
|
||||
$table->foreignId('recipient_user_id')->nullable()->constrained('users')->nullOnDelete()
|
||||
->comment('수신자 회원 ID (회원인 경우)');
|
||||
$table->string('recipient_identifier', 255)->comment('수신자 식별자 (채널별: 이메일, 디바이스토큰, user_id 등)');
|
||||
$table->string('recipient_name', 255)->nullable()->comment('수신자 표시명 (발송 시점 스냅샷)');
|
||||
$table->foreignId('sender_user_id')->nullable()->constrained('users')->nullOnDelete()
|
||||
->comment('발송자 회원 ID (null=시스템 자동)');
|
||||
$table->string('subject', 500)->nullable()->comment('렌더링된 제목');
|
||||
$table->longText('body')->nullable()->comment('렌더링된 본문');
|
||||
$table->string('status', 20)->default('sent')->comment('상태: sent, failed, skipped');
|
||||
$table->text('error_message')->nullable()->comment('에러 메시지');
|
||||
$table->string('source', 100)->nullable()->comment('발송 출처: notification, test_mail 등');
|
||||
$table->timestamp('sent_at')->useCurrent()->comment('발송 시각');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('channel', 'idx_notification_logs_channel');
|
||||
$table->index('notification_type', 'idx_notification_logs_type');
|
||||
$table->index('status', 'idx_notification_logs_status');
|
||||
$table->index('sent_at', 'idx_notification_logs_sent_at');
|
||||
$table->index('recipient_user_id', 'idx_notification_logs_recipient_user');
|
||||
$table->index('recipient_identifier', 'idx_notification_logs_recipient_id');
|
||||
$table->index(['extension_type', 'extension_identifier'], 'idx_notification_logs_extension');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 발송 이력 테이블 삭제.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notification_logs');
|
||||
}
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 알림 템플릿에 click_url 컬럼 추가.
|
||||
*
|
||||
* 알림센터에서 알림 클릭 시 이동할 URL 패턴을 저장합니다.
|
||||
* 변수 플레이스홀더 사용 가능 (예: /mypage/orders/{order_number}).
|
||||
* nullable — 미정의 시 프론트엔드 fallback 경로 사용.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('notification_templates', function (Blueprint $table) {
|
||||
$table->string('click_url', 500)
|
||||
->nullable()
|
||||
->after('body')
|
||||
->comment('알림 클릭 시 이동 URL 패턴 (변수 치환 가능, 예: /mypage/orders/{order_number})');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* click_url 컬럼 제거.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('notification_templates', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('notification_templates', 'click_url')) {
|
||||
$table->dropColumn('click_url');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 알림 템플릿에 수신자 규칙 컬럼을 추가합니다.
|
||||
*
|
||||
* 채널별 독립 수신자 설정을 위해 templates 레벨에 recipients를 배치합니다.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('notification_templates', function (Blueprint $table) {
|
||||
$table->text('recipients')
|
||||
->nullable()
|
||||
->comment('수신자 규칙 JSON ([{type, value, relation, exclude_trigger_user}])')
|
||||
->after('click_url');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 수신자 규칙 컬럼을 제거합니다.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('notification_templates', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('notification_templates', 'recipients')) {
|
||||
$table->dropColumn('recipients');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+39
@@ -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
|
||||
{
|
||||
/**
|
||||
* notification_definitions 테이블에 user_overrides 컬럼 추가.
|
||||
*
|
||||
* 사용자가 수정한 필드명 목록을 JSON 으로 저장하여 시더 재실행 시 보존합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('notification_definitions', function (Blueprint $table) {
|
||||
$table->text('user_overrides')->nullable()->after('is_default')
|
||||
->comment('유저가 수정한 필드명 목록 (예: ["name", "is_active"])');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides 컬럼 삭제.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasColumn('notification_definitions', 'user_overrides')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('notification_definitions', function (Blueprint $table) {
|
||||
$table->dropColumn('user_overrides');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* schedules 테이블에 user_overrides 컬럼 추가 (사전 설계).
|
||||
*
|
||||
* 현재는 시더 없이 사용자 수동 생성 중심이지만, 향후 코어/모듈이 기본 스케줄
|
||||
* 시더를 도입하면 syncOrCreateFromUpgrade 호출만 추가하면 됩니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->text('user_overrides')->nullable()->after('is_active')
|
||||
->comment('유저가 수정한 필드명 목록 (예: ["expression", "command", "timeout"])');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides 컬럼 삭제.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasColumn('schedules', 'user_overrides')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->dropColumn('user_overrides');
|
||||
});
|
||||
}
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 모듈/플러그인 테이블에 vendor_mode 컬럼 추가.
|
||||
*
|
||||
* 마지막 설치 시 사용된 vendor 모드(auto/composer/bundled)를 기록하여,
|
||||
* 업데이트 시 동일 모드를 상속할 수 있도록 합니다.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('modules', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('modules', 'vendor_mode')) {
|
||||
$table->string('vendor_mode', 16)
|
||||
->default('auto')
|
||||
->after('status')
|
||||
->comment('Vendor 설치 모드 (auto|composer|bundled)');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('plugins', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('plugins', 'vendor_mode')) {
|
||||
$table->string('vendor_mode', 16)
|
||||
->default('auto')
|
||||
->after('status')
|
||||
->comment('Vendor 설치 모드 (auto|composer|bundled)');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 마이그레이션 롤백.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('modules', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('modules', 'vendor_mode')) {
|
||||
$table->dropColumn('vendor_mode');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('plugins', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('plugins', 'vendor_mode')) {
|
||||
$table->dropColumn('vendor_mode');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -19,7 +19,7 @@ class DatabaseSeeder extends Seeder
|
||||
AdminUserSeeder::class,
|
||||
RolePermissionSeeder::class,
|
||||
CoreAdminMenuSeeder::class,
|
||||
MailTemplateSeeder::class,
|
||||
NotificationDefinitionSeeder::class,
|
||||
]);
|
||||
|
||||
// 샘플 시더 (--sample 옵션 시에만 실행)
|
||||
@@ -29,7 +29,7 @@ class DatabaseSeeder extends Seeder
|
||||
|
||||
$this->call([
|
||||
Sample\DummyUserSeeder::class,
|
||||
Sample\MailSendLogSeeder::class,
|
||||
Sample\NotificationLogSeeder::class,
|
||||
Sample\TemplateSeeder::class,
|
||||
Sample\TemplateLayoutVersionSeeder::class,
|
||||
Sample\ActivityLogSampleSeeder::class,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\MailTemplate;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MailTemplateSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 코어 메일 템플릿을 시딩합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('코어 메일 템플릿 시딩 시작...');
|
||||
|
||||
$templates = $this->getDefaultTemplates();
|
||||
|
||||
foreach ($templates as $data) {
|
||||
MailTemplate::updateOrCreate(
|
||||
['type' => $data['type']],
|
||||
[
|
||||
'subject' => $data['subject'],
|
||||
'body' => $data['body'],
|
||||
'variables' => $data['variables'],
|
||||
'is_active' => true,
|
||||
'is_default' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$this->command->info(" - {$data['type']} 템플릿 등록 완료");
|
||||
}
|
||||
|
||||
$this->command->info('코어 메일 템플릿 시딩 완료 ('.count($templates).'종)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 템플릿 데이터를 반환합니다.
|
||||
* 정의는 config/core.php의 mail_templates에서 읽습니다.
|
||||
*
|
||||
* 리셋 기능에서도 사용하기 위해 public으로 제공합니다.
|
||||
*
|
||||
* @return array<int, array{type: string, subject: array, body: array, variables: array}> 기본 템플릿 배열
|
||||
*/
|
||||
public function getDefaultTemplates(): array
|
||||
{
|
||||
return config('core.mail_templates', []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Extension\Helpers\NotificationSyncHelper;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class NotificationDefinitionSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 코어 알림 정의 및 템플릿을 시딩합니다.
|
||||
*
|
||||
* 데이터 정합성 (upsert + stale cleanup) 은 NotificationSyncHelper 에 위임하며,
|
||||
* 사용자 수정(user_overrides) 은 helper 내부 HasUserOverrides trait 를 통해 보존됩니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command?->info('코어 알림 정의 시딩 시작...');
|
||||
|
||||
$helper = app(NotificationSyncHelper::class);
|
||||
$definitions = $this->getDefaultDefinitions();
|
||||
$definedTypes = [];
|
||||
|
||||
foreach ($definitions as $data) {
|
||||
$definition = $helper->syncDefinition($data);
|
||||
$definedTypes[] = $definition->type;
|
||||
|
||||
$definedChannels = [];
|
||||
foreach ($data['templates'] as $template) {
|
||||
$helper->syncTemplate($definition->id, $template);
|
||||
$definedChannels[] = $template['channel'];
|
||||
}
|
||||
|
||||
// 완전 동기화: seeder 에서 제거된 channel 의 template 삭제
|
||||
$helper->cleanupStaleTemplates($definition->id, $definedChannels);
|
||||
|
||||
$this->command?->info(" - {$data['type']} 알림 정의 등록 완료");
|
||||
}
|
||||
|
||||
// 완전 동기화: seeder 에서 제거된 코어 definition 삭제 (cascade 로 template 도 정리)
|
||||
$helper->cleanupStaleDefinitions('core', 'core', $definedTypes);
|
||||
|
||||
$this->command?->info('코어 알림 정의 시딩 완료 ('.count($definitions).'종)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 코어 기본 알림 정의 데이터를 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultDefinitions(): array
|
||||
{
|
||||
return [
|
||||
$this->welcomeDefinition(),
|
||||
$this->resetPasswordDefinition(),
|
||||
$this->passwordChangedDefinition(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 환영 알림 정의.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function welcomeDefinition(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'welcome',
|
||||
'hook_prefix' => 'core.auth',
|
||||
'extension_type' => 'core',
|
||||
'extension_identifier' => 'core',
|
||||
'name' => ['ko' => '회원가입 환영', 'en' => 'Welcome'],
|
||||
'description' => ['ko' => '회원가입 완료 시 발송되는 환영 알림', 'en' => 'Welcome notification sent upon registration'],
|
||||
'channels' => ['mail', 'database'],
|
||||
'hooks' => ['core.auth.after_register'],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'action_url', 'description' => '로그인 페이지 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
'templates' => [
|
||||
[
|
||||
'channel' => 'mail',
|
||||
'recipients' => [['type' => 'trigger_user']],
|
||||
'subject' => [
|
||||
'ko' => '[{app_name}] 회원가입을 환영합니다',
|
||||
'en' => '[{app_name}] Welcome to Our Service',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 환영합니다!</h1>'
|
||||
.'<p>{app_name}에 가입해 주셔서 감사합니다.</p>'
|
||||
.'<p>이제 모든 서비스를 이용하실 수 있습니다. 아래 버튼을 클릭하여 로그인해 주세요.</p>'
|
||||
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">로그인하기</a></td></tr></table>'
|
||||
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Welcome, {name}!</h1>'
|
||||
.'<p>Thank you for joining {app_name}.</p>'
|
||||
.'<p>You now have access to all our services. Click the button below to log in.</p>'
|
||||
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">Log In</a></td></tr></table>'
|
||||
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
],
|
||||
[
|
||||
'channel' => 'database',
|
||||
'recipients' => [['type' => 'trigger_user']],
|
||||
'subject' => ['ko' => '회원가입을 환영합니다', 'en' => 'Welcome to our service'],
|
||||
'body' => ['ko' => '{name}님, {app_name}에 가입해 주셔서 감사합니다.', 'en' => 'Welcome {name}, thank you for joining {app_name}.'],
|
||||
'click_url' => '/mypage',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 알림 정의.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function resetPasswordDefinition(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'reset_password',
|
||||
'hook_prefix' => 'core.auth',
|
||||
'extension_type' => 'core',
|
||||
'extension_identifier' => 'core',
|
||||
'name' => ['ko' => '비밀번호 재설정', 'en' => 'Password Reset'],
|
||||
'description' => ['ko' => '비밀번호 재설정 요청 시 발송되는 알림', 'en' => 'Notification sent when password reset is requested'],
|
||||
'channels' => ['mail', 'database'],
|
||||
'hooks' => ['core.auth.after_reset_password_request'],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'action_url', 'description' => '비밀번호 재설정 URL'],
|
||||
['key' => 'expire_minutes', 'description' => '링크 만료 시간(분)'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
'templates' => [
|
||||
[
|
||||
'channel' => 'mail',
|
||||
'recipients' => [['type' => 'trigger_user']],
|
||||
'subject' => [
|
||||
'ko' => '[{app_name}] 비밀번호 재설정 안내',
|
||||
'en' => '[{app_name}] Password Reset Request',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
.'<p>비밀번호 재설정 요청을 받았습니다. 아래 버튼을 클릭하여 비밀번호를 재설정해 주세요.</p>'
|
||||
.'<p>이 링크는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p>'
|
||||
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">비밀번호 재설정</a></td></tr></table>'
|
||||
.'<p>비밀번호 재설정을 요청하지 않으셨다면, 이 이메일을 무시해 주세요.</p>'
|
||||
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
.'<p>We received a request to reset your password. Click the button below to set a new password.</p>'
|
||||
.'<p>This link will expire in <strong>{expire_minutes} minutes</strong>.</p>'
|
||||
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">Reset Password</a></td></tr></table>'
|
||||
.'<p>If you did not request a password reset, please ignore this email.</p>'
|
||||
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
],
|
||||
[
|
||||
'channel' => 'database',
|
||||
'recipients' => [['type' => 'trigger_user']],
|
||||
'subject' => ['ko' => '비밀번호 재설정 안내', 'en' => 'Password Reset Request'],
|
||||
'body' => ['ko' => '{name}님, 비밀번호 재설정이 요청되었습니다. 본인이 요청하지 않았다면 이 알림을 무시해 주세요.', 'en' => '{name}, a password reset has been requested. If you did not request this, please ignore this notification.'],
|
||||
'click_url' => '{action_url}',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 완료 알림 정의.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function passwordChangedDefinition(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'password_changed',
|
||||
'hook_prefix' => 'core.auth',
|
||||
'extension_type' => 'core',
|
||||
'extension_identifier' => 'core',
|
||||
'name' => ['ko' => '비밀번호 변경', 'en' => 'Password Changed'],
|
||||
'description' => ['ko' => '비밀번호 변경 완료 시 발송되는 알림', 'en' => 'Notification sent when password is changed'],
|
||||
'channels' => ['mail', 'database'],
|
||||
'hooks' => ['core.auth.after_password_changed'],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'action_url', 'description' => '로그인 페이지 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
'templates' => [
|
||||
[
|
||||
'channel' => 'mail',
|
||||
'recipients' => [['type' => 'trigger_user']],
|
||||
'subject' => [
|
||||
'ko' => '[{app_name}] 비밀번호가 변경되었습니다',
|
||||
'en' => '[{app_name}] Your Password Has Been Changed',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
.'<p>계정의 비밀번호가 성공적으로 변경되었습니다.</p>'
|
||||
.'<p><strong>본인이 변경하지 않았다면, 즉시 고객 지원팀에 문의하시기 바랍니다.</strong></p>'
|
||||
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">로그인하기</a></td></tr></table>'
|
||||
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
.'<p>Your account password has been successfully changed.</p>'
|
||||
.'<p><strong>If you did not make this change, please contact our support team immediately.</strong></p>'
|
||||
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">Log In</a></td></tr></table>'
|
||||
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
],
|
||||
[
|
||||
'channel' => 'database',
|
||||
'recipients' => [['type' => 'trigger_user']],
|
||||
'subject' => ['ko' => '비밀번호가 변경되었습니다', 'en' => 'Your password has been changed'],
|
||||
'body' => ['ko' => '{name}님, 비밀번호가 변경되었습니다. 본인이 변경하지 않았다면 즉시 고객 지원팀에 문의하시기 바랍니다.', 'en' => '{name}, your password has been changed. If you did not make this change, please contact support immediately.'],
|
||||
'click_url' => '/mypage/change-password',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,11 @@ class RolePermissionSeeder extends Seeder
|
||||
* 계층형 권한을 생성합니다.
|
||||
* 구조: 모듈(1레벨) → 카테고리(2레벨) → 개별 권한(3레벨)
|
||||
* 정의는 config/core.php의 permissions에서 읽습니다.
|
||||
*
|
||||
* type 결정 우선순위 (CoreUpdateService::syncCoreRolesAndPermissions와 동일):
|
||||
* - 개별 권한: 명시된 type > 상위 카테고리 type
|
||||
* - 카테고리: 명시된 type > 하위가 모두 user이면 User, 그 외 Admin
|
||||
* - 모듈 루트: 명시된 type > Admin
|
||||
*/
|
||||
private function createHierarchicalPermissions(): void
|
||||
{
|
||||
@@ -70,7 +75,9 @@ class RolePermissionSeeder extends Seeder
|
||||
'description' => $moduleConfig['description'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'type' => PermissionType::Admin,
|
||||
'type' => isset($moduleConfig['type'])
|
||||
? PermissionType::from($moduleConfig['type'])
|
||||
: PermissionType::Admin,
|
||||
'order' => $moduleConfig['order'],
|
||||
'parent_id' => null,
|
||||
]);
|
||||
@@ -79,6 +86,19 @@ class RolePermissionSeeder extends Seeder
|
||||
$categories = $permConfig['categories'];
|
||||
|
||||
foreach ($categories as $categoryData) {
|
||||
// 카테고리 type 결정: 명시 > 하위 권한 type 자동 추론
|
||||
$childTypes = collect($categoryData['permissions'] ?? [])
|
||||
->map(fn ($p) => $p['type'] ?? 'admin')
|
||||
->unique();
|
||||
|
||||
$categoryType = ($childTypes->count() === 1 && $childTypes->first() === 'user')
|
||||
? PermissionType::User
|
||||
: PermissionType::Admin;
|
||||
|
||||
if (isset($categoryData['type'])) {
|
||||
$categoryType = PermissionType::from($categoryData['type']);
|
||||
}
|
||||
|
||||
// 2레벨: 카테고리 생성
|
||||
$category = Permission::create([
|
||||
'identifier' => $categoryData['identifier'],
|
||||
@@ -86,20 +106,25 @@ class RolePermissionSeeder extends Seeder
|
||||
'description' => $categoryData['description'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'type' => PermissionType::Admin,
|
||||
'type' => $categoryType,
|
||||
'order' => $categoryData['order'],
|
||||
'parent_id' => $coreModule->id,
|
||||
]);
|
||||
|
||||
// 3레벨: 개별 권한 생성
|
||||
foreach ($categoryData['permissions'] as $permData) {
|
||||
// 개별 권한 type: 명시 우선, 없으면 카테고리 type 상속
|
||||
$permissionType = isset($permData['type'])
|
||||
? PermissionType::from($permData['type'])
|
||||
: $categoryType;
|
||||
|
||||
$permissionData = [
|
||||
'identifier' => $permData['identifier'],
|
||||
'name' => $permData['name'],
|
||||
'description' => $permData['description'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'type' => PermissionType::Admin,
|
||||
'type' => $permissionType,
|
||||
'order' => $permData['order'],
|
||||
'parent_id' => $category->id,
|
||||
];
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Database\Seeders\Sample;
|
||||
use App\Enums\ActivityLogType;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Attachment;
|
||||
use App\Models\MailTemplate;
|
||||
use App\Models\NotificationTemplate;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Role;
|
||||
use App\Models\Schedule;
|
||||
@@ -80,7 +80,7 @@ class ActivityLogSampleSeeder extends Seeder
|
||||
$count += $this->seedAttachmentLogs($admins);
|
||||
|
||||
// 메일 템플릿 로그
|
||||
$count += $this->seedMailTemplateLogs($admins);
|
||||
$count += $this->seedNotificationTemplateLogs($admins);
|
||||
|
||||
// 모듈/플러그인 설정 로그
|
||||
$count += $this->seedExtensionSettingsLogs($admins);
|
||||
@@ -810,35 +810,35 @@ class ActivityLogSampleSeeder extends Seeder
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @return int 생성된 레코드 수
|
||||
*/
|
||||
private function seedMailTemplateLogs(Collection $admins): int
|
||||
private function seedNotificationTemplateLogs(Collection $admins): int
|
||||
{
|
||||
$mailTemplates = MailTemplate::get();
|
||||
if ($mailTemplates->isEmpty()) {
|
||||
$this->command->info(' - 메일 템플릿 데이터 없음 - 메일 템플릿 로그 스킵');
|
||||
$templates = NotificationTemplate::with('definition')->get();
|
||||
if ($templates->isEmpty()) {
|
||||
$this->command->info(' - 알림 템플릿 데이터 없음 - 알림 템플릿 로그 스킵');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$morphType = (new MailTemplate)->getMorphClass();
|
||||
$morphType = (new NotificationTemplate)->getMorphClass();
|
||||
|
||||
$actions = [
|
||||
[
|
||||
'action' => 'mail_template.update', 'key' => 'mail_template_update', 'loggable' => true,
|
||||
'params' => fn ($mt) => ['template_name' => $mt->type],
|
||||
'changes' => fn ($mt) => null,
|
||||
'properties' => fn ($mt) => ['template_type' => $mt->type, 'locale' => ['ko', 'en'][array_rand(['ko', 'en'])]],
|
||||
'action' => 'notification_template.update', 'key' => 'notification_template_update', 'loggable' => true,
|
||||
'params' => fn ($t) => ['template_name' => $t->definition?->type ?? $t->channel],
|
||||
'changes' => fn ($t) => null,
|
||||
'properties' => fn ($t) => ['template_type' => $t->definition?->type ?? '', 'channel' => $t->channel, 'locale' => ['ko', 'en'][array_rand(['ko', 'en'])]],
|
||||
],
|
||||
[
|
||||
'action' => 'mail_template.toggle_active', 'key' => 'mail_template_toggle_active', 'loggable' => true,
|
||||
'params' => fn ($mt) => ['template_name' => $mt->type],
|
||||
'changes' => fn ($mt) => null,
|
||||
'properties' => fn ($mt) => ['template_type' => $mt->type, 'is_active' => (bool) mt_rand(0, 1)],
|
||||
'action' => 'notification_template.toggle_active', 'key' => 'notification_template_toggle_active', 'loggable' => true,
|
||||
'params' => fn ($t) => ['template_name' => $t->definition?->type ?? $t->channel],
|
||||
'changes' => fn ($t) => null,
|
||||
'properties' => fn ($t) => ['template_type' => $t->definition?->type ?? '', 'channel' => $t->channel, 'is_active' => (bool) mt_rand(0, 1)],
|
||||
],
|
||||
];
|
||||
|
||||
$count = $this->generateResourceLogs($mailTemplates, $admins, ActivityLogType::Admin, $morphType, $actions);
|
||||
$count = $this->generateResourceLogs($templates, $admins, ActivityLogType::Admin, $morphType, $actions);
|
||||
|
||||
$this->command->info(" - 메일 템플릿 로그: {$count}건");
|
||||
$this->command->info(" - 알림 템플릿 로그: {$count}건");
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Sample;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Enums\MailSendStatus;
|
||||
use App\Models\MailSendLog;
|
||||
use App\Traits\HasSeederCounts;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* 코어 메일 발송 이력 개발용 시더
|
||||
*
|
||||
* 코어 메일 템플릿(welcome, reset_password, password_changed)을 기반으로
|
||||
* 발송 이력 샘플 데이터를 생성합니다.
|
||||
*
|
||||
* 개발/테스트 용도이므로 설치 시 자동 실행되지 않습니다.
|
||||
* 수동 실행: php artisan db:seed --class=MailSendLogSeeder
|
||||
*/
|
||||
class MailSendLogSeeder extends Seeder
|
||||
{
|
||||
use HasSeederCounts;
|
||||
|
||||
/**
|
||||
* 코어 메일 템플릿 유형별 제목 패턴
|
||||
*
|
||||
* @var array<string, array{ko: string, en: string}>
|
||||
*/
|
||||
private array $templateSubjects = [
|
||||
'welcome' => [
|
||||
'ko' => '[그누보드7] 회원가입을 환영합니다',
|
||||
'en' => '[Gnuboard7] Welcome to Our Service',
|
||||
],
|
||||
'reset_password' => [
|
||||
'ko' => '[그누보드7] 비밀번호 재설정 안내',
|
||||
'en' => '[Gnuboard7] Password Reset Request',
|
||||
],
|
||||
'password_changed' => [
|
||||
'ko' => '[그누보드7] 비밀번호가 변경되었습니다',
|
||||
'en' => '[Gnuboard7] Your Password Has Been Changed',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$count = $this->getSeederCount('mail_send_logs', 30);
|
||||
|
||||
$this->command->info('코어 메일 발송 이력 시딩 시작...');
|
||||
|
||||
$templateTypes = array_keys($this->templateSubjects);
|
||||
$recipients = $this->getRecipients();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$templateType = $templateTypes[array_rand($templateTypes)];
|
||||
$recipient = $recipients[array_rand($recipients)];
|
||||
$status = $this->randomStatus();
|
||||
$sentAt = $this->randomSentAt();
|
||||
|
||||
MailSendLog::create([
|
||||
'recipient_email' => $recipient['email'],
|
||||
'recipient_name' => $recipient['name'],
|
||||
'subject' => $this->templateSubjects[$templateType]['ko'],
|
||||
'template_type' => $templateType,
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'source' => 'notification',
|
||||
'status' => $status->value,
|
||||
'error_message' => $status === MailSendStatus::Failed
|
||||
? $this->randomErrorMessage()
|
||||
: null,
|
||||
'sent_at' => $sentAt,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info("코어 메일 발송 이력 시딩 완료 ({$count}건)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 샘플 수신자 목록을 반환합니다.
|
||||
*
|
||||
* @return array<int, array{email: string, name: string}>
|
||||
*/
|
||||
private function getRecipients(): array
|
||||
{
|
||||
return [
|
||||
['email' => 'hong@example.com', 'name' => '홍길동'],
|
||||
['email' => 'kim@example.com', 'name' => '김철수'],
|
||||
['email' => 'lee@example.com', 'name' => '이영희'],
|
||||
['email' => 'park@example.com', 'name' => '박민수'],
|
||||
['email' => 'choi@example.com', 'name' => '최지은'],
|
||||
['email' => 'jung@example.com', 'name' => '정수민'],
|
||||
['email' => 'kang@example.com', 'name' => '강하늘'],
|
||||
['email' => 'yoon@example.com', 'name' => '윤서연'],
|
||||
['email' => 'john@example.com', 'name' => 'John Doe'],
|
||||
['email' => 'jane@example.com', 'name' => 'Jane Smith'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 가중치 기반 랜덤 상태를 반환합니다.
|
||||
*
|
||||
* @return MailSendStatus
|
||||
*/
|
||||
private function randomStatus(): MailSendStatus
|
||||
{
|
||||
$rand = mt_rand(1, 100);
|
||||
|
||||
if ($rand <= 80) {
|
||||
return MailSendStatus::Sent;
|
||||
} elseif ($rand <= 95) {
|
||||
return MailSendStatus::Failed;
|
||||
}
|
||||
|
||||
return MailSendStatus::Skipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 90일 내 랜덤 발송 시각을 반환합니다.
|
||||
*
|
||||
* @return Carbon
|
||||
*/
|
||||
private function randomSentAt(): Carbon
|
||||
{
|
||||
return Carbon::now()->subDays(mt_rand(0, 90))->subHours(mt_rand(0, 23))->subMinutes(mt_rand(0, 59));
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 에러 메시지를 반환합니다.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function randomErrorMessage(): string
|
||||
{
|
||||
$messages = [
|
||||
'Connection timed out after 30 seconds',
|
||||
'SMTP server returned 550: Mailbox not found',
|
||||
'DNS resolution failed for mail server',
|
||||
'Authentication failed: Invalid credentials',
|
||||
'Message rejected: Recipient address rejected',
|
||||
];
|
||||
|
||||
return $messages[array_rand($messages)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Sample;
|
||||
|
||||
use App\Enums\NotificationLogStatus;
|
||||
use App\Models\NotificationLog;
|
||||
use App\Traits\HasSeederCounts;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* 알림 발송 이력 개발용 시더
|
||||
*
|
||||
* 다양한 채널/타입의 알림 발송 이력 샘플 데이터를 생성합니다.
|
||||
* 개발/테스트 용도이므로 설치 시 자동 실행되지 않습니다.
|
||||
*/
|
||||
class NotificationLogSeeder extends Seeder
|
||||
{
|
||||
use HasSeederCounts;
|
||||
|
||||
/**
|
||||
* 시더 실행.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$count = $this->getSeederCount('notification_logs', 50);
|
||||
|
||||
$this->command->info('알림 발송 이력 시딩 시작...');
|
||||
|
||||
$channels = ['mail', 'database'];
|
||||
$types = [
|
||||
['type' => 'welcome', 'ext_type' => 'core', 'ext_id' => 'core'],
|
||||
['type' => 'reset_password', 'ext_type' => 'core', 'ext_id' => 'core'],
|
||||
['type' => 'password_changed', 'ext_type' => 'core', 'ext_id' => 'core'],
|
||||
['type' => 'order_confirmed', 'ext_type' => 'module', 'ext_id' => 'sirsoft-ecommerce'],
|
||||
['type' => 'order_shipped', 'ext_type' => 'module', 'ext_id' => 'sirsoft-ecommerce'],
|
||||
['type' => 'new_comment', 'ext_type' => 'module', 'ext_id' => 'sirsoft-board'],
|
||||
['type' => 'new_post_admin', 'ext_type' => 'module', 'ext_id' => 'sirsoft-board'],
|
||||
];
|
||||
$recipients = $this->getRecipients();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$channel = $channels[array_rand($channels)];
|
||||
$typeInfo = $types[array_rand($types)];
|
||||
$recipient = $recipients[array_rand($recipients)];
|
||||
$status = $this->randomStatus();
|
||||
$sentAt = Carbon::now()->subDays(mt_rand(0, 30))->subHours(mt_rand(0, 23))->subMinutes(mt_rand(0, 59));
|
||||
|
||||
NotificationLog::create([
|
||||
'channel' => $channel,
|
||||
'notification_type' => $typeInfo['type'],
|
||||
'extension_type' => $typeInfo['ext_type'],
|
||||
'extension_identifier' => $typeInfo['ext_id'],
|
||||
'recipient_identifier' => $channel === 'mail' ? $recipient['email'] : (string) ($i + 1),
|
||||
'recipient_name' => $recipient['name'],
|
||||
'subject' => "[G7] {$typeInfo['type']} 알림",
|
||||
'status' => $status->value,
|
||||
'error_message' => $status === NotificationLogStatus::Failed ? 'SMTP connection refused' : null,
|
||||
'source' => 'notification',
|
||||
'sent_at' => $sentAt,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info("알림 발송 이력 시딩 완료 ({$count}건)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 샘플 수신자 목록.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getRecipients(): array
|
||||
{
|
||||
return [
|
||||
['email' => 'hong@example.com', 'name' => '홍길동'],
|
||||
['email' => 'kim@example.com', 'name' => '김철수'],
|
||||
['email' => 'lee@example.com', 'name' => '이영희'],
|
||||
['email' => 'park@example.com', 'name' => '박민수'],
|
||||
['email' => 'john@example.com', 'name' => 'John Doe'],
|
||||
['email' => 'jane@example.com', 'name' => 'Jane Smith'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 가중치 기반 랜덤 상태.
|
||||
*
|
||||
* @return NotificationLogStatus
|
||||
*/
|
||||
private function randomStatus(): NotificationLogStatus
|
||||
{
|
||||
$rand = mt_rand(1, 100);
|
||||
|
||||
if ($rand <= 80) {
|
||||
return NotificationLogStatus::Sent;
|
||||
} elseif ($rand <= 95) {
|
||||
return NotificationLogStatus::Failed;
|
||||
}
|
||||
|
||||
return NotificationLogStatus::Skipped;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user