v7.0.0-beta.1 release
This commit is contained in:
commit
6595fd0eb5
3967 files changed
+1875383
No files matched your search
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\AttachmentSourceType;
|
||||
use App\Models\Attachment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* 첨부파일 팩토리
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Attachment>
|
||||
*/
|
||||
class AttachmentFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 모델 클래스
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Attachment::class;
|
||||
|
||||
/**
|
||||
* 기본 상태 정의
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$filename = $this->faker->word() . '.jpg';
|
||||
$storedFilename = Str::uuid() . '.jpg';
|
||||
|
||||
return [
|
||||
'attachmentable_type' => null,
|
||||
'attachmentable_id' => null,
|
||||
'source_type' => AttachmentSourceType::Core,
|
||||
'source_identifier' => null,
|
||||
'hash' => Str::random(12),
|
||||
'original_filename' => $filename,
|
||||
'stored_filename' => $storedFilename,
|
||||
'disk' => 'local',
|
||||
'path' => 'attachments/' . date('Y/m/d') . '/' . $storedFilename,
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => $this->faker->numberBetween(1024, 1024 * 1024),
|
||||
'collection' => 'default',
|
||||
'order' => 0,
|
||||
'meta' => null,
|
||||
'created_by' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 첨부파일
|
||||
*/
|
||||
public function image(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'mime_type' => $this->faker->randomElement(['image/jpeg', 'image/png', 'image/gif', 'image/webp']),
|
||||
'original_filename' => $this->faker->word() . '.' . $this->faker->randomElement(['jpg', 'png', 'gif', 'webp']),
|
||||
'meta' => [
|
||||
'width' => $this->faker->numberBetween(100, 1920),
|
||||
'height' => $this->faker->numberBetween(100, 1080),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF 첨부파일
|
||||
*/
|
||||
public function pdf(): static
|
||||
{
|
||||
$filename = $this->faker->word() . '.pdf';
|
||||
$storedFilename = Str::uuid() . '.pdf';
|
||||
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'mime_type' => 'application/pdf',
|
||||
'original_filename' => $filename,
|
||||
'stored_filename' => $storedFilename,
|
||||
'path' => 'attachments/' . date('Y/m/d') . '/' . $storedFilename,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 첨부파일
|
||||
*/
|
||||
public function document(): static
|
||||
{
|
||||
$extension = $this->faker->randomElement(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']);
|
||||
$filename = $this->faker->word() . '.' . $extension;
|
||||
$storedFilename = Str::uuid() . '.' . $extension;
|
||||
|
||||
$mimeTypes = [
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'ppt' => 'application/vnd.ms-powerpoint',
|
||||
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
];
|
||||
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'mime_type' => $mimeTypes[$extension],
|
||||
'original_filename' => $filename,
|
||||
'stored_filename' => $storedFilename,
|
||||
'path' => 'attachments/' . date('Y/m/d') . '/' . $storedFilename,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자가 업로드한 상태
|
||||
*/
|
||||
public function uploadedBy(User $user): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'created_by' => $user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 컬렉션 지정
|
||||
*/
|
||||
public function inCollection(string $collection): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'collection' => $collection,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 소스 타입
|
||||
*/
|
||||
public function fromModule(string $identifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => AttachmentSourceType::Module,
|
||||
'source_identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 소스 타입
|
||||
*/
|
||||
public function fromPlugin(string $identifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => AttachmentSourceType::Plugin,
|
||||
'source_identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 모델에 첨부
|
||||
*/
|
||||
public function attachedTo(string $type, int $id): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'attachmentable_type' => $type,
|
||||
'attachmentable_id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 정렬 순서 지정
|
||||
*/
|
||||
public function withOrder(int $order): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'order' => $order,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\LayoutExtensionType;
|
||||
use App\Enums\LayoutSourceType;
|
||||
use App\Models\LayoutExtension;
|
||||
use App\Models\Template;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<LayoutExtension>
|
||||
*/
|
||||
class LayoutExtensionFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<LayoutExtension>
|
||||
*/
|
||||
protected $model = LayoutExtension::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$vendor = fake()->randomElement(['sirsoft', 'acme', 'demo']);
|
||||
|
||||
return [
|
||||
'template_id' => Template::factory(),
|
||||
'extension_type' => fake()->randomElement(LayoutExtensionType::cases()),
|
||||
'target_name' => fake()->randomElement(['sidebar-top', 'sidebar-bottom', 'header-actions', 'admin/dashboard', 'admin/settings']),
|
||||
'source_type' => fake()->randomElement(LayoutSourceType::cases()),
|
||||
'source_identifier' => $vendor.'-'.fake()->slug(1),
|
||||
'content' => $this->generateExtensionData(),
|
||||
'priority' => fake()->numberBetween(1, 100),
|
||||
'is_active' => true,
|
||||
'override_target' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 확장 데이터 생성
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generateExtensionData(): array
|
||||
{
|
||||
return [
|
||||
'component' => [
|
||||
'type' => 'basic',
|
||||
'name' => fake()->randomElement(['Button', 'Div', 'Card', 'Alert']),
|
||||
'props' => [
|
||||
'className' => 'test-class',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension Point 타입 상태
|
||||
*/
|
||||
public function extensionPoint(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'extension_type' => LayoutExtensionType::ExtensionPoint,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay 타입 상태
|
||||
*/
|
||||
public function overlay(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'extension_type' => LayoutExtensionType::Overlay,
|
||||
'content' => [
|
||||
'target_component_id' => 'component-'.fake()->uuid(),
|
||||
'position' => fake()->randomElement(['append_child', 'prepend', 'replace']),
|
||||
'component' => [
|
||||
'type' => 'basic',
|
||||
'name' => 'Div',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 출처 상태
|
||||
*/
|
||||
public function fromModule(string $identifier = null): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => LayoutSourceType::Module,
|
||||
'source_identifier' => $identifier ?? 'sirsoft-ecommerce',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 출처 상태
|
||||
*/
|
||||
public function fromPlugin(string $identifier = null): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => LayoutSourceType::Plugin,
|
||||
'source_identifier' => $identifier ?? 'sirsoft-analytics',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 출처 상태 (오버라이드용)
|
||||
*/
|
||||
public function fromTemplate(string $identifier = null): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => LayoutSourceType::Template,
|
||||
'source_identifier' => $identifier ?? 'sirsoft-admin_basic',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 오버라이드 타겟 설정
|
||||
*/
|
||||
public function overriding(string $targetIdentifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'override_target' => $targetIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비활성 상태
|
||||
*/
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 우선순위 설정
|
||||
*/
|
||||
public function withPriority(int $priority): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'priority' => $priority,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 타겟 설정
|
||||
*/
|
||||
public function targeting(string $targetName): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'target_name' => $targetName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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,87 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Menu>
|
||||
*/
|
||||
class MenuFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 다국어 필드 생성
|
||||
*/
|
||||
protected function generateTranslatableField(callable $generator): array
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$data = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$data[$locale] = $generator();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->generateTranslatableField(fn () => $this->faker->words(2, true)),
|
||||
'slug' => $this->faker->unique()->slug(),
|
||||
'url' => '/admin/'.$this->faker->slug(),
|
||||
'icon' => 'fas fa-'.$this->faker->randomElement(['home', 'user', 'cog', 'chart', 'file']),
|
||||
'parent_id' => null,
|
||||
'order' => $this->faker->numberBetween(1, 100),
|
||||
'is_active' => true,
|
||||
'module_id' => null,
|
||||
'created_by' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 비활성화된 메뉴 상태
|
||||
*/
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 메뉴 상태
|
||||
*/
|
||||
public function moduleMenu(int $moduleId = 1): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'module_id' => $moduleId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 자식 메뉴 상태
|
||||
*/
|
||||
public function childMenu(int $parentId): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'parent_id' => $parentId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코어 메뉴 상태 (기본값과 동일하지만 명시적)
|
||||
*/
|
||||
public function coreMenu(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'module_id' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Module>
|
||||
*/
|
||||
class ModuleFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 다국어 필드 생성
|
||||
*/
|
||||
protected function generateTranslatableField(callable $generator): array
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$data = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$data[$locale] = $generator();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'identifier' => $this->faker->unique()->slug(2),
|
||||
'vendor' => $this->faker->word(),
|
||||
'name' => $this->generateTranslatableField(fn () => $this->faker->words(2, true)),
|
||||
'version' => $this->faker->numerify('#.#.#'),
|
||||
'status' => 'active',
|
||||
'description' => $this->generateTranslatableField(fn () => $this->faker->sentence()),
|
||||
'config' => null,
|
||||
'metadata' => null,
|
||||
'created_by' => null,
|
||||
'updated_by' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Permission>
|
||||
*/
|
||||
class PermissionFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 다국어 필드 생성
|
||||
*/
|
||||
protected function generateTranslatableField(callable $generator): array
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$data = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$data[$locale] = $generator();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'identifier' => $this->faker->unique()->slug(2),
|
||||
'name' => $this->generateTranslatableField(fn () => $this->faker->words(2, true)),
|
||||
'description' => $this->generateTranslatableField(fn () => $this->faker->sentence()),
|
||||
'extension_type' => null,
|
||||
'extension_identifier' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템(코어) 권한 상태
|
||||
*/
|
||||
public function system(): static
|
||||
{
|
||||
return $this->core();
|
||||
}
|
||||
|
||||
/**
|
||||
* 코어 권한 상태
|
||||
*/
|
||||
public function core(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 관리 권한 상태
|
||||
*/
|
||||
public function userManagement(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 권한 상태
|
||||
*
|
||||
* @param string $identifier 모듈 식별자
|
||||
*/
|
||||
public function forModule(string $identifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 권한 상태
|
||||
*
|
||||
* @param string $identifier 플러그인 식별자
|
||||
*/
|
||||
public function forPlugin(string $identifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'extension_type' => ExtensionOwnerType::Plugin,
|
||||
'extension_identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Plugin>
|
||||
*/
|
||||
class PluginFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 다국어 필드 생성
|
||||
*/
|
||||
protected function generateTranslatableField(callable $generator): array
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$data = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$data[$locale] = $generator();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'identifier' => $this->faker->unique()->slug(2),
|
||||
'vendor' => $this->faker->word(),
|
||||
'name' => $this->generateTranslatableField(fn () => $this->faker->words(2, true)),
|
||||
'version' => $this->faker->numerify('#.#.#'),
|
||||
'latest_version' => null,
|
||||
'description' => $this->generateTranslatableField(fn () => $this->faker->sentence()),
|
||||
'status' => 'active',
|
||||
'update_available' => false,
|
||||
'hooks' => null,
|
||||
'github_url' => null,
|
||||
'github_changelog_url' => null,
|
||||
'metadata' => null,
|
||||
'created_by' => null,
|
||||
'updated_by' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Role>
|
||||
*/
|
||||
class RoleFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 다국어 필드 생성
|
||||
*/
|
||||
protected function generateTranslatableField(callable $generator): array
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$data = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$data[$locale] = $generator();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'identifier' => $this->faker->unique()->slug(1),
|
||||
'name' => $this->generateTranslatableField(fn () => $this->faker->words(2, true)),
|
||||
'description' => $this->generateTranslatableField(fn () => $this->faker->sentence()),
|
||||
'is_active' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 비활성화된 역할 상태
|
||||
*/
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 역할
|
||||
*/
|
||||
public function admin(): static
|
||||
{
|
||||
// 기본 번역 정의
|
||||
$defaultTranslations = [
|
||||
'name' => [
|
||||
'ko' => '관리자',
|
||||
'en' => 'Administrator',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '시스템의 모든 권한을 가진 관리자입니다.',
|
||||
'en' => 'Full system administrator with all permissions.',
|
||||
],
|
||||
];
|
||||
|
||||
// config에서 로케일 가져오기
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$nameTranslations = [];
|
||||
$descriptionTranslations = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
// 정의된 번역이 있으면 사용, 없으면 영어 fallback
|
||||
$nameTranslations[$locale] = $defaultTranslations['name'][$locale]
|
||||
?? $defaultTranslations['name']['en'];
|
||||
$descriptionTranslations[$locale] = $defaultTranslations['description'][$locale]
|
||||
?? $defaultTranslations['description']['en'];
|
||||
}
|
||||
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'identifier' => 'admin',
|
||||
'name' => $nameTranslations,
|
||||
'description' => $descriptionTranslations,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 편집자 역할
|
||||
*/
|
||||
public function editor(): static
|
||||
{
|
||||
// 기본 번역 정의
|
||||
$defaultTranslations = [
|
||||
'name' => [
|
||||
'ko' => '편집자',
|
||||
'en' => 'Editor',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '콘텐츠를 편집할 수 있는 편집자입니다.',
|
||||
'en' => 'Content editor with editing permissions.',
|
||||
],
|
||||
];
|
||||
|
||||
// config에서 로케일 가져오기
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$nameTranslations = [];
|
||||
$descriptionTranslations = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
// 정의된 번역이 있으면 사용, 없으면 영어 fallback
|
||||
$nameTranslations[$locale] = $defaultTranslations['name'][$locale]
|
||||
?? $defaultTranslations['name']['en'];
|
||||
$descriptionTranslations[$locale] = $defaultTranslations['description'][$locale]
|
||||
?? $defaultTranslations['description']['en'];
|
||||
}
|
||||
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'identifier' => 'editor',
|
||||
'name' => $nameTranslations,
|
||||
'description' => $descriptionTranslations,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Template>
|
||||
*/
|
||||
class TemplateFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 다국어 필드 생성
|
||||
*/
|
||||
protected function generateTranslatableField(callable $generator): array
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$data = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$data[$locale] = $generator();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$vendor = fake()->randomElement(['sirsoft', 'johndoe', 'acmecorp']);
|
||||
$type = fake()->randomElement(['admin', 'user']);
|
||||
|
||||
return [
|
||||
'identifier' => $vendor.'-'.fake()->slug(2),
|
||||
'vendor' => $vendor,
|
||||
'name' => $this->generateTranslatableField(fn () => fake()->words(2, true)),
|
||||
'version' => fake()->semver(),
|
||||
'latest_version' => fake()->semver(),
|
||||
'update_available' => fake()->boolean(30),
|
||||
'type' => $type,
|
||||
'status' => fake()->randomElement(['active', 'inactive']),
|
||||
'description' => $this->generateTranslatableField(fn () => fake()->sentence()),
|
||||
'user_modified_at' => fake()->boolean(50) ? fake()->dateTime() : null,
|
||||
'github_url' => fake()->url(),
|
||||
'github_changelog_url' => fake()->url(),
|
||||
'metadata' => [
|
||||
'author' => fake()->name(),
|
||||
'license' => 'MIT',
|
||||
],
|
||||
'created_by' => null,
|
||||
'updated_by' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 한국어만 있는 템플릿 상태
|
||||
*/
|
||||
public function koreanOnly(): static
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$nameTranslations = [];
|
||||
$descriptionTranslations = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$nameTranslations[$locale] = $locale === 'ko' ? fake()->words(2, true) : '';
|
||||
$descriptionTranslations[$locale] = $locale === 'ko' ? fake()->sentence() : '';
|
||||
}
|
||||
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'name' => $nameTranslations,
|
||||
'description' => $descriptionTranslations,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 영어만 있는 템플릿 상태
|
||||
*/
|
||||
public function englishOnly(): static
|
||||
{
|
||||
$locales = config('app.translatable_locales', ['ko', 'en']);
|
||||
$nameTranslations = [];
|
||||
$descriptionTranslations = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$nameTranslations[$locale] = $locale === 'en' ? fake()->words(2, true) : '';
|
||||
$descriptionTranslations[$locale] = $locale === 'en' ? fake()->sentence() : '';
|
||||
}
|
||||
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'name' => $nameTranslations,
|
||||
'description' => $descriptionTranslations,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\LayoutSourceType;
|
||||
use App\Models\Template;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\TemplateLayout>
|
||||
*/
|
||||
class TemplateLayoutFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
// 유니크 제약조건 (template_id, name, source_type)을 위해 고유한 name 생성
|
||||
$layoutName = fake()->unique()->slug(3);
|
||||
|
||||
return [
|
||||
'template_id' => Template::factory(),
|
||||
'name' => $layoutName,
|
||||
'content' => [
|
||||
'version' => '1.0.0',
|
||||
'layout_name' => $layoutName,
|
||||
'endpoint' => '/api/'.fake()->word(),
|
||||
'components' => [],
|
||||
'data_sources' => [],
|
||||
'metadata' => [],
|
||||
],
|
||||
'extends' => null,
|
||||
'source_type' => LayoutSourceType::Template,
|
||||
'source_identifier' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 소스 타입으로 설정
|
||||
*/
|
||||
public function fromModule(string $moduleIdentifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => LayoutSourceType::Module,
|
||||
'source_identifier' => $moduleIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 소스 타입으로 설정
|
||||
*/
|
||||
public function fromPlugin(string $pluginIdentifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => LayoutSourceType::Plugin,
|
||||
'source_identifier' => $pluginIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 오버라이드로 설정
|
||||
*/
|
||||
public function asOverride(string $templateIdentifier): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'source_type' => LayoutSourceType::Template,
|
||||
'source_identifier' => $templateIdentifier,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\TemplateLayout;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\TemplateLayoutVersion>
|
||||
*/
|
||||
class TemplateLayoutVersionFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'layout_id' => TemplateLayout::factory(),
|
||||
'version' => fake()->unique()->numberBetween(1, 1000),
|
||||
'content' => [
|
||||
'version' => '1.0.0',
|
||||
'layout_name' => fake()->word(),
|
||||
'components' => [],
|
||||
],
|
||||
'changes_summary' => json_encode([
|
||||
'added' => fake()->numberBetween(1, 20),
|
||||
'removed' => fake()->numberBetween(0, 10),
|
||||
'is_restored' => false,
|
||||
'restored_from' => null,
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => Str::orderedUuid()->toString(),
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
'is_super' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* SEO 캐시 통계 테이블을 생성합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('seo_cache_stats', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->string('url')->comment('요청 URL');
|
||||
$table->string('locale', 5)->comment('로케일');
|
||||
$table->string('layout_name')->nullable()->comment('사용된 레이아웃명');
|
||||
$table->string('module_identifier')->nullable()->comment('레이아웃 소유 모듈 식별자');
|
||||
$table->string('type')->comment('유형 (hit 또는 miss)');
|
||||
$table->integer('response_time_ms')->nullable()->comment('렌더링 소요 시간 (ms)');
|
||||
$table->timestamp('created_at')->useCurrent()->comment('생성 시각');
|
||||
|
||||
$table->index('url', 'idx_seo_cache_stats_url');
|
||||
$table->index('type', 'idx_seo_cache_stats_type');
|
||||
$table->index('created_at', 'idx_seo_cache_stats_created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO 캐시 통계 테이블을 삭제합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('seo_cache_stats');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('사용자 이름');
|
||||
$table->string('nickname', 50)->nullable()->comment('닉네임');
|
||||
$table->string('email')->unique()->comment('이메일 주소');
|
||||
$table->timestamp('email_verified_at')->nullable()->comment('이메일 인증 일시');
|
||||
$table->string('password')->comment('비밀번호 해시');
|
||||
$table->string('language', 5)->default('ko')->comment('사용자 언어 설정 (ko: 한국어, en: 영어)');
|
||||
$table->boolean('is_super')->default(false)->index()->comment('슈퍼 관리자 여부 (삭제 불가, 권한 관리 가능)');
|
||||
$table->string('timezone', 50)->nullable()->default('Asia/Seoul')->comment('사용자 시간대 (예: Asia/Seoul, UTC)');
|
||||
$table->string('country', 2)->nullable()->index()->comment('국가 코드 (ISO 3166-1 alpha-2)');
|
||||
$table->string('status', 20)->default('active')->index()->comment('계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴)');
|
||||
$table->string('homepage')->nullable()->comment('홈페이지 URL');
|
||||
$table->string('mobile', 20)->nullable()->index()->comment('휴대폰 번호');
|
||||
$table->string('phone', 20)->nullable()->index()->comment('전화번호');
|
||||
$table->string('zipcode', 10)->nullable()->comment('우편번호');
|
||||
$table->string('address')->nullable()->comment('기본 주소');
|
||||
$table->string('address_detail')->nullable()->comment('상세 주소');
|
||||
$table->text('signature')->nullable()->comment('서명');
|
||||
$table->text('bio')->nullable()->comment('자기소개');
|
||||
$table->text('admin_memo')->nullable()->comment('관리자 메모');
|
||||
$table->string('ip_address', 45)->nullable()->index()->comment('마지막 접속 IP 주소');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->timestamp('last_login_at')->nullable()->comment('마지막 로그인 일시');
|
||||
$table->timestamp('withdrawn_at')->nullable()->comment('탈퇴 일시');
|
||||
$table->timestamp('blocked_at')->nullable()->comment('차단 일시');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->comment('시스템 사용자 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary()->comment('비밀번호 재설정 요청 이메일');
|
||||
$table->string('token')->comment('비밀번호 재설정 토큰');
|
||||
$table->timestamp('created_at')->nullable()->comment('생성일시');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->comment('비밀번호 재설정 토큰 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary()->comment('세션 ID');
|
||||
$table->foreignId('user_id')->nullable()->index()->comment('사용자 ID');
|
||||
$table->string('ip_address', 45)->nullable()->comment('IP 주소');
|
||||
$table->text('user_agent')->nullable()->comment('사용자 에이전트');
|
||||
$table->longText('payload')->comment('세션 데이터');
|
||||
$table->integer('last_activity')->index()->comment('마지막 활동 시간');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('sessions', function (Blueprint $table) {
|
||||
$table->comment('사용자 세션 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary()->comment('캐시 키');
|
||||
$table->mediumText('value')->comment('캐시 값');
|
||||
$table->integer('expiration')->comment('만료 시간');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('cache', function (Blueprint $table) {
|
||||
$table->comment('시스템 캐시 데이터');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary()->comment('락 키');
|
||||
$table->string('owner')->comment('락 소유자');
|
||||
$table->integer('expiration')->comment('만료 시간');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('cache_locks', function (Blueprint $table) {
|
||||
$table->comment('캐시 락 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index()->comment('큐 이름');
|
||||
$table->longText('payload')->comment('작업 페이로드');
|
||||
$table->unsignedTinyInteger('attempts')->comment('시도 횟수');
|
||||
$table->unsignedInteger('reserved_at')->nullable()->comment('예약 시간');
|
||||
$table->unsignedInteger('available_at')->comment('실행 가능 시간');
|
||||
$table->unsignedInteger('created_at')->comment('생성 시간');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('jobs', function (Blueprint $table) {
|
||||
$table->comment('작업 큐 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
}
|
||||
};
|
||||
@@ -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('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary()->comment('배치 ID');
|
||||
$table->string('name')->comment('배치 이름');
|
||||
$table->integer('total_jobs')->comment('전체 작업 수');
|
||||
$table->integer('pending_jobs')->comment('대기 중인 작업 수');
|
||||
$table->integer('failed_jobs')->comment('실패한 작업 수');
|
||||
$table->longText('failed_job_ids')->comment('실패한 작업 ID 목록');
|
||||
$table->mediumText('options')->nullable()->comment('배치 옵션');
|
||||
$table->integer('cancelled_at')->nullable()->comment('취소 시간');
|
||||
$table->integer('created_at')->comment('생성 시간');
|
||||
$table->integer('finished_at')->nullable()->comment('완료 시간');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('job_batches', function (Blueprint $table) {
|
||||
$table->comment('작업 배치 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('job_batches');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique()->comment('고유 식별자');
|
||||
$table->text('connection')->comment('연결 정보');
|
||||
$table->text('queue')->comment('큐 이름');
|
||||
$table->longText('payload')->comment('작업 페이로드');
|
||||
$table->longText('exception')->comment('예외 정보');
|
||||
$table->timestamp('failed_at')->useCurrent()->comment('실패 시간');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('failed_jobs', function (Blueprint $table) {
|
||||
$table->comment('실패한 작업 로그');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->text('name')->comment('토큰 이름');
|
||||
$table->string('token', 64)->unique()->comment('액세스 토큰');
|
||||
$table->text('abilities')->nullable()->comment('토큰 권한');
|
||||
$table->timestamp('last_used_at')->nullable()->comment('마지막 사용 시간');
|
||||
$table->timestamp('expires_at')->nullable()->index()->comment('만료 시간');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->comment('개인 액세스 토큰 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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('permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->comment('상위 권한 ID (계층 구조)');
|
||||
$table->string('identifier')->unique()->comment('권한명 (예: users.create, menus.read)');
|
||||
$table->text('name')->comment('권한 이름 (다국어 JSON)');
|
||||
$table->mediumText('description')->nullable()->comment('권한 설명 (다국어 JSON)');
|
||||
$table->string('extension_type', 20)->nullable()->comment('확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의)');
|
||||
$table->string('extension_identifier', 255)->nullable()->comment('확장 식별자 (예: core, sirsoft-board, sirsoft-payment)');
|
||||
$table->string('type', 20)->comment('권한 타입');
|
||||
$table->string('resource_route_key', 50)->nullable()->comment('리소스 라우트 파라미터명 (예: user, menu, product)');
|
||||
$table->string('owner_key', 50)->nullable()->comment('소유자 식별 컬럼명 (예: id, created_by, user_id)');
|
||||
$table->integer('order')->default(0)->comment('정렬 순서');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('parent_id');
|
||||
$table->index('type');
|
||||
$table->index(['extension_type', 'extension_identifier'], 'idx_permissions_extension');
|
||||
$table->foreign('parent_id')->references('id')->on('permissions')->cascadeOnDelete();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('permissions', function (Blueprint $table) {
|
||||
$table->comment('시스템 권한 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('permissions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('permission_hooks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('permission_id')->constrained('permissions')->cascadeOnDelete();
|
||||
$table->string('hook_name')->index()->comment('훅 이름 (예: core.attachment.download)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['permission_id', 'hook_name']);
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('permission_hooks', function (Blueprint $table) {
|
||||
$table->comment('권한-훅 매핑 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('permission_hooks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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('roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('identifier')->unique()->comment('역할명 (예: admin, user, manager)');
|
||||
$table->text('name')->comment('역할 이름 (다국어 JSON)');
|
||||
$table->mediumText('description')->nullable()->comment('역할 설명 (다국어 JSON)');
|
||||
$table->string('extension_type', 20)->nullable()->comment('확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의)');
|
||||
$table->string('extension_identifier', 255)->nullable()->comment('확장 식별자 (예: core, sirsoft-board, sirsoft-payment)');
|
||||
$table->text('user_overrides')->nullable()->comment('유저가 수정한 필드명 목록 (예: ["name", "permissions"])');
|
||||
$table->boolean('is_active')->default(true)->comment('활성화 상태 (1: 활성, 0: 비활성)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['extension_type', 'extension_identifier'], 'idx_roles_extension');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->comment('사용자 역할 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('roles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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('role_permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('role_id')->constrained('roles')->cascadeOnDelete();
|
||||
$table->foreignId('permission_id')->constrained('permissions')->cascadeOnDelete();
|
||||
$table->enum('scope_type', ['self', 'role'])->nullable()->comment('접근 스코프 (null: 전체, self: 본인, role: 소유역할)');
|
||||
$table->timestamp('granted_at')->useCurrent()->comment('권한 부여 일시');
|
||||
$table->foreignId('granted_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['role_id', 'permission_id']);
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('role_permissions', function (Blueprint $table) {
|
||||
$table->comment('역할-권한 관계 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('role_permissions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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('menus', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->text('name')->comment('메뉴 이름 (다국어 JSON)');
|
||||
$table->string('slug')->unique()->comment('메뉴 슬러그');
|
||||
$table->string('url')->nullable()->comment('메뉴 URL');
|
||||
$table->string('icon')->nullable()->comment('메뉴 아이콘');
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->comment('상위 메뉴 ID');
|
||||
$table->integer('order')->default(0)->comment('메뉴 순서');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 상태 (1: 활성, 0: 비활성)');
|
||||
$table->string('extension_type', 20)->nullable()->comment('확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의)');
|
||||
$table->string('extension_identifier', 255)->nullable()->comment('확장 식별자 (예: core, sirsoft-board, sirsoft-payment)');
|
||||
$table->text('user_overrides')->nullable()->comment('유저가 수정한 필드명 목록 (예: ["name", "icon", "order"])');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('등록자 ID');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['parent_id', 'order']);
|
||||
$table->index(['is_active', 'order']);
|
||||
$table->index(['extension_type', 'extension_identifier'], 'idx_menus_extension');
|
||||
$table->foreign('parent_id')->references('id')->on('menus')->cascadeOnDelete();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('menus', function (Blueprint $table) {
|
||||
$table->comment('시스템 메뉴 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('menus');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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('menu_permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('menu_id')->constrained('menus')->cascadeOnDelete();
|
||||
$table->foreignId('role_id')->nullable()->constrained('roles')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->cascadeOnDelete();
|
||||
$table->enum('permission_type', ['read', 'write', 'delete'])->default('read')->comment('권한 유형 (read: 읽기, write: 쓰기, delete: 삭제)');
|
||||
$table->boolean('is_allowed')->default(true)->comment('허용 여부 (1: 허용, 0: 거부)');
|
||||
$table->timestamp('granted_at')->useCurrent()->comment('권한 부여 일시');
|
||||
$table->foreignId('granted_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['menu_id', 'role_id']);
|
||||
$table->index(['menu_id', 'user_id']);
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('menu_permissions', function (Blueprint $table) {
|
||||
$table->comment('메뉴별 접근 권한 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('menu_permissions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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('role_menus', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('role_id')->constrained('roles')->cascadeOnDelete();
|
||||
$table->foreignId('menu_id')->constrained('menus')->cascadeOnDelete();
|
||||
$table->enum('permission_type', ['read', 'write', 'delete'])->default('read')->comment('권한 타입 (read: 읽기/접근, write: 수정, delete: 삭제)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['role_id', 'menu_id', 'permission_type'], 'uk_role_menu_permission');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('role_menus', function (Blueprint $table) {
|
||||
$table->comment('역할별 메뉴 접근 권한 (피벗 테이블)');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('role_menus');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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('user_roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('role_id')->constrained('roles')->cascadeOnDelete();
|
||||
$table->timestamp('assigned_at')->useCurrent()->comment('역할 할당 일시');
|
||||
$table->foreignId('assigned_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'role_id']);
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('user_roles', function (Blueprint $table) {
|
||||
$table->comment('사용자-역할 관계 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_roles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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('user_consents', function (Blueprint $table) {
|
||||
$table->id()->comment('동의 이력 ID');
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete()->comment('사용자 ID');
|
||||
$table->string('consent_type', 30)->comment('동의 유형: terms, privacy');
|
||||
$table->timestamp('agreed_at')->comment('동의 일시');
|
||||
$table->timestamp('revoked_at')->nullable()->comment('철회 일시 (향후 플러그인 확장용)');
|
||||
$table->string('ip_address', 45)->nullable()->comment('동의 시 IP 주소 (IPv6 대응)');
|
||||
$table->timestamp('created_at')->nullable()->comment('생성 일시');
|
||||
|
||||
$table->index(['user_id', 'consent_type'], 'user_consents_user_type_index');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('user_consents', function (Blueprint $table) {
|
||||
$table->comment('사용자 약관 동의 이력');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_consents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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('modules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('identifier')->unique()->comment('모듈 고유 식별자 (vendor-module 형식)');
|
||||
$table->string('vendor')->index()->comment('벤더/개발자명');
|
||||
$table->text('name')->comment('모듈 이름 (다국어 JSON)');
|
||||
$table->mediumText('description')->nullable()->comment('모듈 설명 (다국어 JSON)');
|
||||
$table->string('github_url', 512)->nullable()->comment('GitHub 저장소 URL');
|
||||
$table->string('github_changelog_url', 512)->nullable()->comment('GitHub 변경 내역 URL');
|
||||
$table->mediumText('metadata')->nullable()->comment('추가 메타데이터');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->string('version')->comment('모듈 버전');
|
||||
$table->string('latest_version', 50)->nullable()->comment('마켓플레이스 최신 버전');
|
||||
$table->enum('status', ['active', 'inactive', 'installing', 'uninstalling', 'updating'])->default('inactive')->index()->comment('상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중)');
|
||||
$table->boolean('update_available')->default(false)->comment('업데이트 가능 여부');
|
||||
$table->string('update_source', 20)->nullable()->comment('업데이트 출처 (github, pending, bundled)');
|
||||
$table->mediumText('config')->nullable()->comment('모듈 설정 정보');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('모듈 생성자 ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('modules', function (Blueprint $table) {
|
||||
$table->comment('시스템 모듈 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('modules');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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('plugins', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('identifier')->unique()->comment('플러그인 고유 식별자 (vendor-plugin 형식)');
|
||||
$table->string('vendor')->index()->comment('벤더/개발자명');
|
||||
$table->text('name')->comment('플러그인 이름 (다국어 JSON)');
|
||||
$table->mediumText('description')->nullable()->comment('플러그인 설명 (다국어 JSON)');
|
||||
$table->string('github_url', 512)->nullable()->comment('GitHub 저장소 URL');
|
||||
$table->mediumText('metadata')->nullable()->comment('추가 메타데이터');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->string('version')->comment('플러그인 버전');
|
||||
$table->string('latest_version', 50)->nullable()->comment('마켓플레이스 최신 버전');
|
||||
$table->boolean('update_available')->default(false)->comment('업데이트 가능 여부');
|
||||
$table->string('update_source', 20)->nullable()->comment('업데이트 출처 (github, pending, bundled)');
|
||||
$table->string('github_changelog_url', 512)->nullable()->comment('GitHub 변경 내역 URL');
|
||||
$table->enum('status', ['active', 'inactive', 'installing', 'uninstalling', 'updating'])->default('inactive')->index()->comment('상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중)');
|
||||
$table->mediumText('hooks')->nullable()->comment('훅 설정 정보');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('플러그인 생성자 ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('plugins', function (Blueprint $table) {
|
||||
$table->comment('시스템 플러그인 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('plugins');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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('templates', function (Blueprint $table) {
|
||||
$table->id()->comment('템플릿 ID');
|
||||
$table->string('identifier')->unique()->comment('템플릿 고유 식별자 (vendor-name 형식, 예: sirsoft-admin_basic)');
|
||||
$table->string('vendor')->index()->comment('벤더/개발자명 (예: sirsoft)');
|
||||
$table->text('name')->nullable()->comment('템플릿 이름 (다국어 JSON)');
|
||||
$table->string('version')->comment('템플릿 버전 (예: 1.0.0)');
|
||||
$table->string('latest_version', 50)->nullable()->comment('마켓플레이스 최신 버전');
|
||||
$table->boolean('update_available')->default(false)->comment('업데이트 가능 여부');
|
||||
$table->string('update_source', 20)->nullable()->comment('업데이트 출처 (github, pending, bundled)');
|
||||
$table->enum('type', ['admin', 'user'])->comment('템플릿 타입 (admin: 관리자용, user: 사용자용)');
|
||||
$table->enum('status', ['active', 'inactive', 'installing', 'uninstalling', 'updating'])->default('inactive')->comment('상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중)');
|
||||
$table->mediumText('description')->nullable()->comment('템플릿 설명 (다국어 JSON)');
|
||||
$table->timestamp('user_modified_at')->nullable()->comment('사용자가 레이아웃을 마지막으로 수정한 시각');
|
||||
$table->string('github_url')->nullable()->comment('GitHub 저장소 URL');
|
||||
$table->string('github_changelog_url', 512)->nullable()->comment('GitHub 변경 내역 URL');
|
||||
$table->mediumText('metadata')->nullable()->comment('추가 메타데이터 (JSON 형식)');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('type');
|
||||
$table->index('status');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('templates', function (Blueprint $table) {
|
||||
$table->comment('시스템 템플릿 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('templates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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_layouts', function (Blueprint $table) {
|
||||
$table->id()->comment('레이아웃 ID');
|
||||
$table->foreignId('template_id')->constrained('templates')->cascadeOnDelete()->comment('템플릿 ID');
|
||||
$table->string('name')->comment('레이아웃 이름 (예: dashboard, users, user_edit)');
|
||||
$table->longText('content')->comment('레이아웃 JSON 내용');
|
||||
$table->string('extends')->nullable()->index()->comment('부모 레이아웃 이름 (예: layouts/_admin_base)');
|
||||
$table->enum('source_type', ['template', 'module', 'plugin'])->default('template')->comment('레이아웃 소스 타입 (template: 템플릿 자체/오버라이드, module: 모듈 기본, plugin: 플러그인 기본)');
|
||||
$table->string('source_identifier', 255)->nullable()->comment('모듈/플러그인 식별자 (source_type이 module/plugin일 때 사용)');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['template_id', 'name', 'source_type'], 'uk_template_layout_source');
|
||||
$table->index(['source_type', 'source_identifier'], 'idx_source');
|
||||
$table->index('deleted_at');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('template_layouts', function (Blueprint $table) {
|
||||
$table->comment('템플릿 레이아웃 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('template_layouts');
|
||||
}
|
||||
};
|
||||
@@ -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_versions', function (Blueprint $table) {
|
||||
$table->id()->comment('버전 ID');
|
||||
$table->foreignId('layout_id')->constrained('template_layouts')->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(['layout_id', 'version'], 'uk_layout_version');
|
||||
$table->index(['layout_id', 'created_at'], 'idx_layout_versions');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('template_layout_versions', function (Blueprint $table) {
|
||||
$table->comment('템플릿 레이아웃 버전 이력');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('template_layout_versions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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_extensions', function (Blueprint $table) {
|
||||
$table->id()->comment('확장 ID');
|
||||
$table->foreignId('template_id')->constrained('templates')->cascadeOnDelete()->comment('템플릿 ID (g7_templates 참조)');
|
||||
$table->string('extension_type', 50)->comment('확장 타입: extension_point=확장점 방식, overlay=ID 기반 오버레이 방식');
|
||||
$table->string('target_name')->comment('타겟 이름 (extension_point: 확장점명, overlay: 레이아웃명)');
|
||||
$table->string('source_type', 50)->comment('출처 타입: template=템플릿(오버라이드용), module=모듈, plugin=플러그인');
|
||||
$table->string('source_identifier', 255)->comment('출처 식별자 (예: sirsoft-ecommerce). 템플릿 오버라이드의 경우 오버라이드 대상 모듈/플러그인 식별자');
|
||||
$table->string('override_target', 255)->nullable()->comment('오버라이드 대상 (source_type=template일 때만 사용). 모듈/플러그인 식별자');
|
||||
$table->longText('content')->comment('확장 정의 JSON (components, injections, data_sources 포함)');
|
||||
$table->integer('priority')->default(100)->comment('우선순위 (낮을수록 먼저 적용, 기본값: 100)');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 상태 (true=활성, false=비활성)');
|
||||
$table->timestamps();
|
||||
$table->softDeletes()->comment('삭제 일시 (soft delete)');
|
||||
|
||||
$table->index(['template_id', 'extension_type', 'target_name', 'is_active'], 'idx_target');
|
||||
$table->index(['source_type', 'source_identifier'], 'idx_source');
|
||||
$table->index(['override_target', 'template_id', 'extension_type', 'target_name'], 'idx_override');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('template_layout_extensions', function (Blueprint $table) {
|
||||
$table->comment('템플릿 레이아웃 확장 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('template_layout_extensions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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('attachments', function (Blueprint $table) {
|
||||
$table->id()->comment('첨부파일 ID');
|
||||
$table->string('attachmentable_type', 255)->nullable()->comment('첨부 대상 모델 타입 (예: App\\Models\\Post)');
|
||||
$table->unsignedBigInteger('attachmentable_id')->nullable()->comment('첨부 대상 모델 ID');
|
||||
$table->enum('source_type', ['core', 'module', 'plugin'])->default('core')->comment('소스 타입: core(코어), module(모듈), plugin(플러그인)');
|
||||
$table->string('source_identifier', 255)->nullable()->comment('모듈/플러그인 식별자 (예: sirsoft-board)');
|
||||
$table->string('hash', 12)->unique()->comment('URL용 고유 해시 (12자)');
|
||||
$table->string('original_filename', 255)->comment('원본 파일명');
|
||||
$table->string('stored_filename', 255)->comment('저장된 파일명 (UUID 기반)');
|
||||
$table->string('disk', 50)->default('local')->comment('스토리지 디스크 (local, s3 등)');
|
||||
$table->string('path', 500)->comment('저장 경로 (디스크 기준 상대 경로)');
|
||||
$table->string('mime_type', 100)->comment('MIME 타입 (예: image/jpeg, application/pdf)');
|
||||
$table->unsignedBigInteger('size')->comment('파일 크기 (바이트)');
|
||||
$table->string('collection', 100)->default('default')->comment('첨부파일 컬렉션/그룹명');
|
||||
$table->unsignedInteger('order')->default(0)->comment('정렬 순서');
|
||||
$table->text('meta')->nullable()->comment('추가 메타데이터 JSON');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['attachmentable_type', 'attachmentable_id'], 'idx_attachmentable');
|
||||
$table->index(['source_type', 'source_identifier'], 'idx_source');
|
||||
$table->index('collection', 'idx_collection');
|
||||
$table->index('deleted_at', 'idx_deleted_at');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('attachments', function (Blueprint $table) {
|
||||
$table->comment('첨부파일 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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('schedules', function (Blueprint $table) {
|
||||
$table->id()->comment('스케줄 ID');
|
||||
$table->string('name', 255)->comment('작업명');
|
||||
$table->text('description')->nullable()->comment('설명');
|
||||
$table->enum('type', ['artisan', 'shell', 'url'])->comment('작업 유형: artisan(Artisan 커맨드), shell(쉘 명령), url(URL 호출)');
|
||||
$table->text('command')->comment('명령어 또는 URL');
|
||||
$table->string('expression', 100)->comment('Cron 표현식');
|
||||
$table->enum('frequency', ['everyMinute', 'hourly', 'daily', 'weekly', 'monthly', 'custom'])->default('custom')->comment('실행 주기: everyMinute(매분), hourly(매시간), daily(매일), weekly(매주), monthly(매월), custom(사용자 정의)');
|
||||
$table->boolean('without_overlapping')->default(false)->comment('중복 실행 방지 여부: 0(허용), 1(방지)');
|
||||
$table->boolean('run_in_maintenance')->default(false)->comment('점검 모드 실행 여부: 0(비실행), 1(실행)');
|
||||
$table->unsignedInteger('timeout')->nullable()->comment('실행 제한 시간 (초)');
|
||||
$table->boolean('is_active')->default(true)->comment('활성화 여부: 0(비활성), 1(활성)');
|
||||
$table->enum('last_result', ['success', 'failed', 'running', 'never'])->default('never')->comment('마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행)');
|
||||
$table->timestamp('last_run_at')->nullable()->comment('마지막 실행 시간');
|
||||
$table->timestamp('next_run_at')->nullable()->comment('다음 실행 예정 시간');
|
||||
$table->string('extension_type', 20)->nullable()->comment('확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의)');
|
||||
$table->string('extension_identifier', 255)->nullable()->comment('확장 식별자 (예: core, sirsoft-board, sirsoft-payment)');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('type', 'idx_type');
|
||||
$table->index('frequency', 'idx_frequency');
|
||||
$table->index('is_active', 'idx_is_active');
|
||||
$table->index('last_result', 'idx_last_result');
|
||||
$table->index('next_run_at', 'idx_next_run_at');
|
||||
$table->index(['extension_type', 'extension_identifier'], 'idx_schedules_extension');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->comment('스케줄 작업 관리');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('schedules');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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('schedule_histories', function (Blueprint $table) {
|
||||
$table->id()->comment('이력 ID');
|
||||
$table->foreignId('schedule_id')->constrained('schedules')->cascadeOnDelete();
|
||||
$table->timestamp('started_at')->comment('실행 시작 시간');
|
||||
$table->timestamp('ended_at')->nullable()->comment('실행 종료 시간');
|
||||
$table->unsignedInteger('duration')->nullable()->comment('실행 시간 (초)');
|
||||
$table->enum('status', ['success', 'failed', 'running'])->default('running')->comment('실행 상태: success(성공), failed(실패), running(실행중)');
|
||||
$table->tinyInteger('exit_code')->nullable()->comment('종료 코드 (0: 성공)');
|
||||
$table->unsignedInteger('memory_usage')->nullable()->comment('메모리 사용량 (bytes)');
|
||||
$table->longText('output')->nullable()->comment('표준 출력');
|
||||
$table->longText('error_output')->nullable()->comment('에러 출력');
|
||||
$table->enum('trigger_type', ['scheduled', 'manual'])->default('scheduled')->comment('트리거 유형: scheduled(예약 실행), manual(수동 실행)');
|
||||
$table->foreignId('triggered_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('started_at', 'idx_started_at');
|
||||
$table->index('status', 'idx_status');
|
||||
$table->index('trigger_type', 'idx_trigger_type');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('schedule_histories', function (Blueprint $table) {
|
||||
$table->comment('스케줄 실행 이력');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('schedule_histories');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->id()->comment('활동 로그 ID');
|
||||
$table->string('log_type', 20)->index()->comment('로그 유형 (admin: 관리자, user: 사용자, system: 시스템)');
|
||||
$table->nullableMorphs('loggable');
|
||||
$table->foreignId('user_id')->nullable()->index()->constrained('users')->nullOnDelete()->comment('사용자 ID');
|
||||
$table->string('action', 50)->index()->comment('액션 유형 (created, updated, deleted, login, export 등)');
|
||||
$table->text('description')->comment('액션 상세 설명');
|
||||
$table->mediumText('properties')->nullable()->comment('변경 상세 데이터 (old/new 값)');
|
||||
$table->string('ip_address', 45)->nullable()->comment('IP 주소 (IPv6 대응)');
|
||||
$table->string('user_agent', 500)->nullable()->comment('User Agent');
|
||||
$table->timestamp('created_at')->useCurrent()->index()->comment('생성일시');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('activity_logs');
|
||||
}
|
||||
};
|
||||
@@ -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('mail_templates', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->string('type', 100)->unique()->comment('템플릿 유형 (welcome, reset_password 등)');
|
||||
$table->text('subject')->comment('다국어 제목 ({"ko": "...", "en": "..."})');
|
||||
$table->mediumText('body')->comment('다국어 HTML 본문 ({"ko": "...", "en": "..."})');
|
||||
$table->text('variables')->nullable()->comment('사용 가능 변수 메타데이터 ([{key, description}])');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 여부 (false = 해당 유형 메일 발송 중단)');
|
||||
$table->boolean('is_default')->default(true)->comment('시더 생성 항목 여부');
|
||||
$table->text('user_overrides')->nullable()->comment('사용자가 수정한 필드명 목록 (코어 업데이트 시 보존)');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('mail_templates', function (Blueprint $table) {
|
||||
$table->comment('코어 메일 템플릿');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mail_templates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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('mail_send_logs', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->string('extension_type', 20)->default('core')->comment('확장 타입 (core, module, plugin)');
|
||||
$table->string('extension_identifier', 100)->default('core')->comment('확장 식별자');
|
||||
$table->string('sender_email', 255)->nullable()->comment('발송자 이메일');
|
||||
$table->string('sender_name', 255)->nullable()->comment('발송자 이름');
|
||||
$table->string('recipient_email', 255)->comment('수신자 이메일');
|
||||
$table->string('recipient_name', 255)->nullable()->comment('수신자 이름');
|
||||
$table->string('subject', 500)->nullable()->comment('이메일 제목');
|
||||
$table->longText('body')->nullable()->comment('이메일 본문 (HTML)');
|
||||
$table->string('template_type', 100)->nullable()->comment('템플릿 유형 (welcome, new_comment 등)');
|
||||
$table->string('source', 100)->nullable()->comment('발송 출처 (notification, test_mail 등)');
|
||||
$table->string('status', 20)->default('sent')->comment('발송 상태 (sent, failed)');
|
||||
$table->text('error_message')->nullable()->comment('실패 시 에러 메시지');
|
||||
$table->timestamp('sent_at')->useCurrent()->comment('발송 시각');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('recipient_email', 'idx_mail_send_logs_recipient');
|
||||
$table->index('template_type', 'idx_mail_send_logs_template');
|
||||
$table->index('sent_at', 'idx_mail_send_logs_sent_at');
|
||||
$table->index(['extension_type', 'extension_identifier'], 'idx_mail_send_logs_extension');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mail_send_logs');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* 사용자 테이블에 UUID 컬럼을 추가합니다.
|
||||
*
|
||||
* 외부 API 응답에서 정수 ID 노출을 방지하기 위해
|
||||
* UUID v7 기반 고유 식별자를 추가합니다.
|
||||
*
|
||||
* - 신규 설치: User 모델 boot() creating 이벤트에서 UUID 자동 생성
|
||||
* - 버전 업그레이드: 업그레이드 스크립트에서 기존 레코드 백필 + NOT NULL 변환
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->nullable()->after('id')->unique()->comment('외부 노출용 UUID v7');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID 컬럼을 제거합니다.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropUnique(['uuid']);
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* activity_logs 테이블 i18n 필드 변경.
|
||||
*
|
||||
* - description 컬럼 삭제 (번역된 텍스트 저장 → i18n 키 기반으로 전환)
|
||||
* - description_key, description_params 컬럼 추가 (실시간 다국어 번역)
|
||||
* - changes 컬럼 추가 (구조화된 변경 이력)
|
||||
* - 복합 인덱스 추가 (loggable + created_at, log_type + action + created_at)
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// SQLite 호환: 컬럼 삭제와 추가를 별도 블록으로 분리
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->dropColumn('description');
|
||||
});
|
||||
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->string('description_key', 150)->nullable()->after('action')
|
||||
->comment('다국어 번역 키 (예: activity_log.description.user_create)');
|
||||
$table->text('description_params')->nullable()->after('description_key')
|
||||
->comment('다국어 번역 파라미터 (예: {"user_id": "abc-123"})');
|
||||
$table->mediumText('changes')->nullable()->after('properties')
|
||||
->comment('구조화된 변경 이력 (필드별 label_key, old, new, type)');
|
||||
|
||||
$table->index(
|
||||
['loggable_type', 'loggable_id', 'created_at'],
|
||||
'idx_activity_logs_loggable'
|
||||
);
|
||||
$table->index(
|
||||
['log_type', 'action', 'created_at'],
|
||||
'idx_activity_logs_type_action_date'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_activity_logs_loggable');
|
||||
$table->dropIndex('idx_activity_logs_type_action_date');
|
||||
$table->dropColumn(['description_key', 'description_params', 'changes']);
|
||||
});
|
||||
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->text('description')->nullable()->after('action')
|
||||
->comment('활동 설명');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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_layout_previews', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->char('token', 36)->unique()->comment('미리보기 URL 토큰 (UUID)');
|
||||
$table->foreignId('template_id')->constrained('templates')->cascadeOnDelete()->comment('템플릿 ID');
|
||||
$table->string('layout_name', 255)->comment('미리보기 대상 레이아웃 이름');
|
||||
$table->longText('content')->comment('편집 중인 레이아웃 JSON');
|
||||
$table->unsignedBigInteger('admin_id')->comment('미리보기 생성 관리자 ID');
|
||||
$table->timestamp('expires_at')->comment('만료 시각');
|
||||
$table->timestamp('created_at')->useCurrent()->comment('생성 시각');
|
||||
|
||||
$table->index('expires_at', 'idx_layout_previews_expires_at');
|
||||
$table->index(['template_id', 'layout_name', 'admin_id'], 'idx_layout_previews_template_layout_admin');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('template_layout_previews');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* activity_logs 테이블 인덱스 추가.
|
||||
*
|
||||
* - description_key: WhereIn 기반 다국어 검색
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$existingIndexes = array_column(Schema::getIndexes('activity_logs'), 'name');
|
||||
|
||||
if (! in_array('idx_activity_logs_description_key', $existingIndexes)) {
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->index('description_key', 'idx_activity_logs_description_key');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('activity_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingIndexes = array_column(Schema::getIndexes('activity_logs'), 'name');
|
||||
|
||||
if (in_array('idx_activity_logs_description_key', $existingIndexes)) {
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_activity_logs_description_key');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* users 테이블 인덱스 추가.
|
||||
*
|
||||
* - created_at: 날짜 범위 필터, 통계, 정렬
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$existingIndexes = array_column(Schema::getIndexes('users'), 'name');
|
||||
|
||||
if (! in_array('idx_users_created_at', $existingIndexes)) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->index('created_at', 'idx_users_created_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('users')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingIndexes = array_column(Schema::getIndexes('users'), 'name');
|
||||
|
||||
if (in_array('idx_users_created_at', $existingIndexes)) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_users_created_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* mail_send_logs 테이블 인덱스 추가.
|
||||
*
|
||||
* - status: sent/failed 상태 필터
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$existingIndexes = array_column(Schema::getIndexes('mail_send_logs'), 'name');
|
||||
|
||||
if (! in_array('idx_mail_send_logs_status', $existingIndexes)) {
|
||||
Schema::table('mail_send_logs', function (Blueprint $table) {
|
||||
$table->index('status', 'idx_mail_send_logs_status');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('mail_send_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingIndexes = array_column(Schema::getIndexes('mail_send_logs'), 'name');
|
||||
|
||||
if (in_array('idx_mail_send_logs_status', $existingIndexes)) {
|
||||
Schema::table('mail_send_logs', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_mail_send_logs_status');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* template_layouts 테이블 인덱스 추가.
|
||||
*
|
||||
* - template_id: 템플릿별 레이아웃 조회
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$existingIndexes = array_column(Schema::getIndexes('template_layouts'), 'name');
|
||||
|
||||
if (! in_array('idx_template_layouts_template_id', $existingIndexes)) {
|
||||
Schema::table('template_layouts', function (Blueprint $table) {
|
||||
$table->index('template_id', 'idx_template_layouts_template_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('template_layouts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingIndexes = array_column(Schema::getIndexes('template_layouts'), 'name');
|
||||
|
||||
if (in_array('idx_template_layouts_template_id', $existingIndexes)) {
|
||||
Schema::table('template_layouts', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_template_layouts_template_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* schedules 테이블 인덱스 추가.
|
||||
*
|
||||
* - created_at: 날짜 범위 필터, 정렬
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$existingIndexes = array_column(Schema::getIndexes('schedules'), 'name');
|
||||
|
||||
if (! in_array('idx_schedules_created_at', $existingIndexes)) {
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->index('created_at', 'idx_schedules_created_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('schedules')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingIndexes = array_column(Schema::getIndexes('schedules'), 'name');
|
||||
|
||||
if (in_array('idx_schedules_created_at', $existingIndexes)) {
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_schedules_created_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?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_layouts', function (Blueprint $table) {
|
||||
$table->string('original_content_hash', 64)
|
||||
->nullable()
|
||||
->after('content')
|
||||
->comment('원본 레이아웃 콘텐츠 SHA-256 해시 (수정 감지용)');
|
||||
$table->unsignedInteger('original_content_size')
|
||||
->nullable()
|
||||
->after('original_content_hash')
|
||||
->comment('원본 레이아웃 콘텐츠 바이트 크기 (변화량 표시용)');
|
||||
});
|
||||
|
||||
// 기존 데이터 hash 백필 (현재 content = 원본으로 간주)
|
||||
DB::table('template_layouts')
|
||||
->whereNull('original_content_hash')
|
||||
->orderBy('id')
|
||||
->chunk(100, function ($layouts) {
|
||||
foreach ($layouts as $layout) {
|
||||
$content = json_decode($layout->content, true);
|
||||
$normalized = json_encode($content, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$hash = hash('sha256', $normalized);
|
||||
$size = strlen($normalized);
|
||||
|
||||
DB::table('template_layouts')
|
||||
->where('id', $layout->id)
|
||||
->update([
|
||||
'original_content_hash' => $hash,
|
||||
'original_content_size' => $size,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('template_layouts', function (Blueprint $table) {
|
||||
$table->dropColumn(['original_content_hash', 'original_content_size']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdminUserSeeder extends Seeder
|
||||
{
|
||||
private array $defaultAdminUser;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$name = getenv('INSTALLER_ADMIN_NAME');
|
||||
$email = getenv('INSTALLER_ADMIN_EMAIL');
|
||||
$password = getenv('INSTALLER_ADMIN_PASSWORD');
|
||||
|
||||
if (! $email || ! $password) {
|
||||
throw new \RuntimeException(
|
||||
'관리자 계정 환경변수가 설정되지 않았습니다. '
|
||||
. 'INSTALLER_ADMIN_EMAIL, INSTALLER_ADMIN_PASSWORD 환경변수를 설정해주세요.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->defaultAdminUser = [
|
||||
'name' => $name ?: 'Administrator',
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'email_verified_at' => now(),
|
||||
'timezone' => 'Asia/Seoul',
|
||||
'language' => getenv('INSTALLER_ADMIN_LANGUAGE') ?: 'ko',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('기본 관리자 사용자 생성을 시작합니다.');
|
||||
|
||||
// 기존 데이터 삭제
|
||||
$this->deleteExistingUsers();
|
||||
|
||||
// 새 데이터 생성
|
||||
$this->createAdminUser($this->defaultAdminUser);
|
||||
|
||||
$this->command->info('기본 관리자 사용자가 성공적으로 생성되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 관리자 사용자를 삭제합니다.
|
||||
*/
|
||||
private function deleteExistingUsers(): void
|
||||
{
|
||||
$deletedCount = User::query()->delete();
|
||||
|
||||
if ($deletedCount > 0) {
|
||||
$this->command->info("기존 관리자 사용자 {$deletedCount}개가 삭제되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 관리자 사용자를 생성합니다.
|
||||
*
|
||||
* 첫 관리자는 슈퍼 관리자로 생성됩니다 (is_super = true).
|
||||
* 슈퍼 관리자는 삭제할 수 없으며, 다른 관리자의 권한을 관리할 수 있습니다.
|
||||
*
|
||||
* @param array $userData 사용자 데이터
|
||||
*/
|
||||
private function createAdminUser(array $userData): void
|
||||
{
|
||||
User::create([
|
||||
'name' => $userData['name'],
|
||||
'email' => $userData['email'],
|
||||
'password' => Hash::make($userData['password']),
|
||||
'email_verified_at' => $userData['email_verified_at'],
|
||||
'timezone' => $userData['timezone'],
|
||||
'language' => $userData['language'],
|
||||
'is_super' => true, // 첫 관리자는 슈퍼 관리자로 설정
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Enums\MenuPermissionType;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CoreAdminMenuSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('코어 관리자 메뉴 생성을 시작합니다.');
|
||||
|
||||
// 기존 데이터 삭제
|
||||
$this->deleteExistingCoreMenus();
|
||||
|
||||
// 관리자 사용자 찾기 (첫 번째 사용자를 관리자로 가정)
|
||||
$admin = User::first();
|
||||
|
||||
// 새 데이터 생성
|
||||
$this->createCoreMenus($admin);
|
||||
|
||||
$this->command->info('코어 관리자 메뉴가 성공적으로 생성되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 코어 메뉴를 삭제합니다.
|
||||
*/
|
||||
private function deleteExistingCoreMenus(): void
|
||||
{
|
||||
$deletedCount = Menu::where('extension_type', ExtensionOwnerType::Core)->delete();
|
||||
|
||||
if ($deletedCount > 0) {
|
||||
$this->command->info("기존 코어 메뉴 {$deletedCount}개가 삭제되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 코어 관리자 메뉴를 생성합니다.
|
||||
* 정의는 config/core.php의 menus에서 읽습니다.
|
||||
*
|
||||
* @param User|null $admin 관리자 사용자
|
||||
*/
|
||||
private function createCoreMenus(?User $admin): void
|
||||
{
|
||||
$coreMenus = config('core.menus');
|
||||
|
||||
$createdMenus = [];
|
||||
foreach ($coreMenus as $menuData) {
|
||||
$createdMenus[] = Menu::create(array_merge($menuData, [
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'created_by' => null,
|
||||
]));
|
||||
}
|
||||
|
||||
// 관리자 역할에 모든 코어 메뉴 read 권한 부여
|
||||
$this->grantAdminRoleToMenus($createdMenus);
|
||||
|
||||
// 매니저 역할에 대시보드 메뉴 read 권한 부여
|
||||
$this->grantManagerDashboardMenu($createdMenus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 역할에 모든 코어 메뉴의 read 권한을 부여합니다.
|
||||
*
|
||||
* @param array<Menu> $menus 생성된 메뉴 목록
|
||||
*/
|
||||
private function grantAdminRoleToMenus(array $menus): void
|
||||
{
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
|
||||
if (! $adminRole) {
|
||||
$this->command->warn('관리자 역할(admin)을 찾을 수 없어 메뉴 권한 부여를 건너뜁니다.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($menus as $menu) {
|
||||
$adminRole->menus()->attach($menu->id, [
|
||||
'permission_type' => MenuPermissionType::Read->value,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info('관리자 역할에 '.count($menus).'개의 코어 메뉴 권한을 부여했습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 매니저 역할에 대시보드 메뉴의 read 권한을 부여합니다.
|
||||
*
|
||||
* @param array<Menu> $menus 생성된 메뉴 목록
|
||||
*/
|
||||
private function grantManagerDashboardMenu(array $menus): void
|
||||
{
|
||||
$managerRole = Role::where('identifier', 'manager')->first();
|
||||
|
||||
if (! $managerRole) {
|
||||
$this->command->warn('매니저 역할(manager)을 찾을 수 없어 대시보드 메뉴 권한 부여를 건너뜁니다.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dashboardMenu = collect($menus)->first(fn (Menu $menu) => $menu->slug === 'admin-dashboard');
|
||||
|
||||
if (! $dashboardMenu) {
|
||||
$this->command->warn('대시보드 메뉴(admin-dashboard)를 찾을 수 없어 매니저 메뉴 권한 부여를 건너뜁니다.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$managerRole->menus()->attach($dashboardMenu->id, [
|
||||
'permission_type' => MenuPermissionType::Read->value,
|
||||
]);
|
||||
|
||||
$this->command->info('매니저 역할에 대시보드 메뉴 권한을 부여했습니다.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Traits\HasSampleSeeders;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use HasSampleSeeders;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// 설치 필수 시더 (항상 실행)
|
||||
$this->call([
|
||||
AdminUserSeeder::class,
|
||||
RolePermissionSeeder::class,
|
||||
CoreAdminMenuSeeder::class,
|
||||
MailTemplateSeeder::class,
|
||||
]);
|
||||
|
||||
// 샘플 시더 (--sample 옵션 시에만 실행)
|
||||
if ($this->shouldIncludeSample()) {
|
||||
$this->command->info('');
|
||||
$this->command->info('=== 코어 샘플 시더 실행 시작 ===');
|
||||
|
||||
$this->call([
|
||||
Sample\DummyUserSeeder::class,
|
||||
Sample\MailSendLogSeeder::class,
|
||||
Sample\TemplateSeeder::class,
|
||||
Sample\TemplateLayoutVersionSeeder::class,
|
||||
Sample\ActivityLogSampleSeeder::class,
|
||||
]);
|
||||
|
||||
$this->command->info('=== 코어 샘플 시더 실행 완료 ===');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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,213 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Enums\PermissionType;
|
||||
use App\Enums\ScopeType;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RolePermissionSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 기본 권한과 역할을 생성합니다.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('기본 권한과 역할 생성을 시작합니다.');
|
||||
|
||||
DB::transaction(function () {
|
||||
// 기존 데이터 삭제
|
||||
$this->deleteExistingData();
|
||||
|
||||
// 계층형 권한 생성
|
||||
$this->createHierarchicalPermissions();
|
||||
|
||||
// 기본 역할 생성
|
||||
$this->createRoles();
|
||||
|
||||
// 관리자 계정에 admin 역할 부여
|
||||
$this->assignAdminRole();
|
||||
});
|
||||
|
||||
$this->command->info('기본 권한과 역할이 성공적으로 생성되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 코어 권한 데이터를 삭제합니다.
|
||||
*
|
||||
* 주의: 역할(Role)은 삭제하지 않습니다. 역할 삭제 시 role_menus, role_users 등
|
||||
* cascade 연결된 피벗 데이터가 함께 삭제되므로, 역할은 updateOrCreate로 보존합니다.
|
||||
*/
|
||||
private function deleteExistingData(): void
|
||||
{
|
||||
// 기존 코어 권한만 삭제 (role_permissions 피벗은 cascade 삭제됨 → 이후 재연결)
|
||||
$deletedPermissions = Permission::where('extension_type', ExtensionOwnerType::Core)->delete();
|
||||
|
||||
if ($deletedPermissions > 0) {
|
||||
$this->command->info("기존 코어 권한 {$deletedPermissions}개가 삭제되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 계층형 권한을 생성합니다.
|
||||
* 구조: 모듈(1레벨) → 카테고리(2레벨) → 개별 권한(3레벨)
|
||||
* 정의는 config/core.php의 permissions에서 읽습니다.
|
||||
*/
|
||||
private function createHierarchicalPermissions(): void
|
||||
{
|
||||
$permConfig = config('core.permissions');
|
||||
$moduleConfig = $permConfig['module'];
|
||||
|
||||
// 1레벨: 코어 모듈
|
||||
$coreModule = Permission::create([
|
||||
'identifier' => $moduleConfig['identifier'],
|
||||
'name' => $moduleConfig['name'],
|
||||
'description' => $moduleConfig['description'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'type' => PermissionType::Admin,
|
||||
'order' => $moduleConfig['order'],
|
||||
'parent_id' => null,
|
||||
]);
|
||||
|
||||
// 2레벨: 카테고리들
|
||||
$categories = $permConfig['categories'];
|
||||
|
||||
foreach ($categories as $categoryData) {
|
||||
// 2레벨: 카테고리 생성
|
||||
$category = Permission::create([
|
||||
'identifier' => $categoryData['identifier'],
|
||||
'name' => $categoryData['name'],
|
||||
'description' => $categoryData['description'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'type' => PermissionType::Admin,
|
||||
'order' => $categoryData['order'],
|
||||
'parent_id' => $coreModule->id,
|
||||
]);
|
||||
|
||||
// 3레벨: 개별 권한 생성
|
||||
foreach ($categoryData['permissions'] as $permData) {
|
||||
$permissionData = [
|
||||
'identifier' => $permData['identifier'],
|
||||
'name' => $permData['name'],
|
||||
'description' => $permData['description'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'type' => PermissionType::Admin,
|
||||
'order' => $permData['order'],
|
||||
'parent_id' => $category->id,
|
||||
];
|
||||
|
||||
// scope 관련 컬럼 (resource_route_key, owner_key) 추가
|
||||
if (isset($permData['resource_route_key'])) {
|
||||
$permissionData['resource_route_key'] = $permData['resource_route_key'];
|
||||
}
|
||||
if (isset($permData['owner_key'])) {
|
||||
$permissionData['owner_key'] = $permData['owner_key'];
|
||||
}
|
||||
|
||||
Permission::create($permissionData);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info('계층형 권한이 생성되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할을 생성(또는 업데이트)하고 권한을 할당합니다.
|
||||
* 정의는 config/core.php의 roles에서 읽습니다.
|
||||
*
|
||||
* updateOrCreate를 사용하여 역할 ID를 보존합니다.
|
||||
* 역할 삭제 시 role_menus, role_users 등 cascade 피벗이 함께 삭제되므로,
|
||||
* 역할은 절대 삭제하지 않고 속성만 업데이트합니다.
|
||||
*/
|
||||
private function createRoles(): void
|
||||
{
|
||||
$roles = config('core.roles');
|
||||
|
||||
foreach ($roles as $roleDef) {
|
||||
$role = Role::updateOrCreate(
|
||||
['identifier' => $roleDef['identifier']],
|
||||
[
|
||||
'name' => $roleDef['name'],
|
||||
'description' => $roleDef['description'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'is_active' => $roleDef['attributes']['is_active'] ?? true,
|
||||
]
|
||||
);
|
||||
|
||||
// 권한별 스코프 맵 (identifier => scope_type value)
|
||||
$scopeMap = $roleDef['permission_scopes'] ?? [];
|
||||
|
||||
// 권한 할당
|
||||
if ($roleDef['permissions'] === 'all_leaf') {
|
||||
// 리프 노드 코어 권한만 할당 (parent_id가 있고, 자식이 없는 권한)
|
||||
$leafPermissions = Permission::where('extension_type', ExtensionOwnerType::Core)
|
||||
->whereNotNull('parent_id')
|
||||
->whereDoesntHave('children')
|
||||
->get();
|
||||
|
||||
// 코어 권한만 sync하고, 모듈/플러그인이 부여한 권한은 보존
|
||||
$nonCorePermissionIds = $role->permissions()
|
||||
->where('extension_type', '!=', ExtensionOwnerType::Core)
|
||||
->pluck('permissions.id')
|
||||
->toArray();
|
||||
|
||||
$allPermissionIds = array_merge($leafPermissions->pluck('id')->toArray(), $nonCorePermissionIds);
|
||||
$role->permissions()->sync($allPermissionIds);
|
||||
} elseif (is_array($roleDef['permissions']) && ! empty($roleDef['permissions'])) {
|
||||
$permissions = Permission::whereIn('identifier', $roleDef['permissions'])->get();
|
||||
|
||||
// scope_type 매핑 (permission_scopes 맵에서 스코프 타입 해석)
|
||||
$coreSyncData = $permissions->mapWithKeys(function ($permission) use ($scopeMap) {
|
||||
$pivotData = [
|
||||
'scope_type' => isset($scopeMap[$permission->identifier])
|
||||
? ScopeType::from($scopeMap[$permission->identifier])
|
||||
: null,
|
||||
];
|
||||
|
||||
return [$permission->id => $pivotData];
|
||||
})->toArray();
|
||||
|
||||
// 모듈/플러그인이 부여한 권한은 보존
|
||||
$nonCorePermissions = $role->permissions()
|
||||
->where('extension_type', '!=', ExtensionOwnerType::Core)
|
||||
->get()
|
||||
->mapWithKeys(fn ($p) => [$p->id => ['scope_type' => $p->pivot->scope_type ?? null]])
|
||||
->toArray();
|
||||
|
||||
$role->permissions()->sync(array_replace($coreSyncData, $nonCorePermissions));
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info('역할이 생성/업데이트되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 계정에 admin 역할을 부여하고 슈퍼 관리자로 설정합니다.
|
||||
*/
|
||||
private function assignAdminRole(): void
|
||||
{
|
||||
$adminUser = User::where('is_super', true)->first();
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
|
||||
if ($adminUser && $adminRole) {
|
||||
$adminUser->roles()->syncWithoutDetaching([$adminRole->id => [
|
||||
'assigned_at' => now(),
|
||||
'assigned_by' => $adminUser->id,
|
||||
]]);
|
||||
|
||||
// 기본 관리자를 슈퍼 관리자로 설정
|
||||
$adminUser->update(['is_super' => true]);
|
||||
|
||||
$this->command->info('관리자 계정에 admin 역할이 부여되고 슈퍼 관리자로 설정되었습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Sample;
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Traits\HasSeederCounts;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class DummyUserSeeder extends Seeder
|
||||
{
|
||||
use HasSeederCounts;
|
||||
|
||||
/**
|
||||
* 영문 이름 목록 (이메일 생성용)
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $englishFirstNames = [
|
||||
'james', 'john', 'michael', 'david', 'william',
|
||||
'emma', 'olivia', 'sophia', 'isabella', 'mia',
|
||||
'robert', 'joseph', 'thomas', 'charles', 'daniel',
|
||||
'emily', 'elizabeth', 'madison', 'abigail', 'charlotte',
|
||||
];
|
||||
|
||||
/**
|
||||
* 영문 성 목록 (이메일 생성용)
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $englishLastNames = [
|
||||
'smith', 'johnson', 'williams', 'brown', 'jones',
|
||||
'garcia', 'miller', 'davis', 'rodriguez', 'martinez',
|
||||
'wilson', 'anderson', 'taylor', 'thomas', 'moore',
|
||||
];
|
||||
|
||||
/**
|
||||
* 더미 사용자를 생성합니다.
|
||||
*
|
||||
* --count=users=N 옵션으로 생성 수를 지정할 수 있습니다.
|
||||
* 500건 초과 시 배치 삽입으로 성능을 최적화합니다.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$count = $this->getSeederCount('users', 100);
|
||||
$this->command->info("더미 사용자 {$count}개 생성을 시작합니다.");
|
||||
|
||||
// 역할 조회
|
||||
$roles = Role::whereIn('identifier', ['admin', 'manager', 'user'])->get()->keyBy('identifier');
|
||||
|
||||
if ($roles->isEmpty()) {
|
||||
$this->command->error('역할이 존재하지 않습니다. RolePermissionSeeder를 먼저 실행해주세요.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$timezones = [
|
||||
'Asia/Seoul',
|
||||
'Asia/Tokyo',
|
||||
'America/New_York',
|
||||
'America/Los_Angeles',
|
||||
'Europe/London',
|
||||
'Europe/Paris',
|
||||
];
|
||||
|
||||
$languages = ['ko', 'en'];
|
||||
|
||||
$statuses = ['active', 'inactive', 'blocked'];
|
||||
|
||||
$countries = ['KR', 'JP', 'US', 'GB', 'FR', 'DE'];
|
||||
|
||||
$useBatchInsert = $count > 500;
|
||||
$batchSize = 500;
|
||||
$batch = [];
|
||||
$roleBatch = [];
|
||||
$createdCount = 0;
|
||||
$hashedPassword = Hash::make('password123');
|
||||
|
||||
$progressBar = $this->command->getOutput()->createProgressBar($count);
|
||||
$progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%');
|
||||
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$firstName = $this->getRandomFirstName();
|
||||
$lastName = $this->getRandomLastName();
|
||||
$name = "{$firstName} {$lastName}";
|
||||
|
||||
// 이메일은 영문 소문자와 숫자만 사용
|
||||
$emailFirstName = $this->englishFirstNames[array_rand($this->englishFirstNames)];
|
||||
$emailLastName = $this->englishLastNames[array_rand($this->englishLastNames)];
|
||||
$email = $emailFirstName.'.'.$emailLastName.$i.'@example.com';
|
||||
|
||||
$userData = [
|
||||
'name' => $name,
|
||||
'nickname' => $firstName.$i,
|
||||
'email' => $email,
|
||||
'password' => $hashedPassword,
|
||||
'email_verified_at' => rand(0, 1) ? now()->subDays(rand(1, 365)) : null,
|
||||
'language' => $languages[array_rand($languages)],
|
||||
'timezone' => $timezones[array_rand($timezones)],
|
||||
'country' => $countries[array_rand($countries)],
|
||||
'status' => $statuses[array_rand($statuses)],
|
||||
'mobile' => '010-'.rand(1000, 9999).'-'.rand(1000, 9999),
|
||||
'bio' => rand(0, 1) ? $this->getRandomBio() : null,
|
||||
'ip_address' => rand(1, 255).'.'.rand(0, 255).'.'.rand(0, 255).'.'.rand(1, 254),
|
||||
'last_login_at' => rand(0, 1) ? now()->subDays(rand(0, 30)) : null,
|
||||
];
|
||||
|
||||
// 역할 부여 (90% user, 8% manager, 2% admin)
|
||||
$roleIdentifier = $this->getRandomRole();
|
||||
|
||||
if ($useBatchInsert) {
|
||||
$userData['created_at'] = now();
|
||||
$userData['updated_at'] = now();
|
||||
$batch[] = $userData;
|
||||
$roleBatch[] = $roleIdentifier;
|
||||
|
||||
// 배치 크기에 도달하면 일괄 삽입
|
||||
if (count($batch) >= $batchSize) {
|
||||
$this->insertBatch($batch, $roleBatch, $roles);
|
||||
$batch = [];
|
||||
$roleBatch = [];
|
||||
}
|
||||
} else {
|
||||
$user = User::create($userData);
|
||||
$role = $roles->get($roleIdentifier);
|
||||
|
||||
if ($role) {
|
||||
$user->roles()->attach($role->id, [
|
||||
'assigned_at' => now(),
|
||||
'assigned_by' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$createdCount++;
|
||||
$progressBar->advance();
|
||||
|
||||
// 100건마다 메모리 정리
|
||||
if ($i % 100 === 0) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
}
|
||||
|
||||
// 남은 배치 삽입
|
||||
if ($useBatchInsert && ! empty($batch)) {
|
||||
$this->insertBatch($batch, $roleBatch, $roles);
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->command->newLine();
|
||||
$this->command->info("더미 사용자 {$createdCount}개가 성공적으로 생성되었습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 배치 삽입 및 역할 부여
|
||||
*
|
||||
* @param array $batch 사용자 데이터 배열
|
||||
* @param array $roleBatch 역할 식별자 배열
|
||||
* @param \Illuminate\Support\Collection $roles 역할 컬렉션
|
||||
* @return void
|
||||
*/
|
||||
private function insertBatch(array $batch, array $roleBatch, $roles): void
|
||||
{
|
||||
// 배치 삽입 전 이메일 목록 저장
|
||||
$emails = array_column($batch, 'email');
|
||||
|
||||
// 배치 삽입
|
||||
User::insert($batch);
|
||||
|
||||
// 삽입된 사용자 조회하여 역할 부여
|
||||
$insertedUsers = User::whereIn('email', $emails)->get()->keyBy('email');
|
||||
$roleAttachments = [];
|
||||
|
||||
foreach ($batch as $index => $userData) {
|
||||
$user = $insertedUsers->get($userData['email']);
|
||||
if ($user) {
|
||||
$role = $roles->get($roleBatch[$index]);
|
||||
if ($role) {
|
||||
$roleAttachments[] = [
|
||||
'user_id' => $user->id,
|
||||
'role_id' => $role->id,
|
||||
'assigned_at' => now(),
|
||||
'assigned_by' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($roleAttachments)) {
|
||||
\Illuminate\Support\Facades\DB::table('user_roles')->insert($roleAttachments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 역할 식별자를 반환합니다.
|
||||
* 90% user, 8% manager, 2% admin 비율
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getRandomRole(): string
|
||||
{
|
||||
$rand = rand(1, 100);
|
||||
|
||||
if ($rand <= 2) {
|
||||
return 'admin';
|
||||
}
|
||||
|
||||
if ($rand <= 10) {
|
||||
return 'manager';
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 이름(First Name)을 반환합니다.
|
||||
*/
|
||||
private function getRandomFirstName(): string
|
||||
{
|
||||
$names = [
|
||||
'민준', '서준', '도윤', '예준', '시우',
|
||||
'하준', '지호', '주원', '지후', '준우',
|
||||
'서연', '서윤', '지우', '서현', '민서',
|
||||
'하은', '하윤', '윤서', '지민', '지유',
|
||||
'James', 'John', 'Michael', 'David', 'William',
|
||||
'Emma', 'Olivia', 'Sophia', 'Isabella', 'Mia',
|
||||
];
|
||||
|
||||
return $names[array_rand($names)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 성(Last Name)을 반환합니다.
|
||||
*/
|
||||
private function getRandomLastName(): string
|
||||
{
|
||||
$names = [
|
||||
'김', '이', '박', '최', '정',
|
||||
'강', '조', '윤', '장', '임',
|
||||
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones',
|
||||
'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez',
|
||||
];
|
||||
|
||||
return $names[array_rand($names)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 자기소개를 반환합니다.
|
||||
*/
|
||||
private function getRandomBio(): string
|
||||
{
|
||||
$bios = [
|
||||
'안녕하세요! 반갑습니다.',
|
||||
'개발을 좋아하는 사람입니다.',
|
||||
'Hello! Nice to meet you.',
|
||||
'새로운 것을 배우는 것을 좋아합니다.',
|
||||
'Software developer passionate about technology.',
|
||||
'항상 배우고 성장하려고 노력합니다.',
|
||||
'Love coding and problem solving.',
|
||||
'커피와 코딩을 좋아합니다.',
|
||||
];
|
||||
|
||||
return $bios[array_rand($bios)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?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,117 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Sample;
|
||||
|
||||
use App\Models\TemplateLayout;
|
||||
use App\Models\TemplateLayoutVersion;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class TemplateLayoutVersionSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 샘플 버전 데이터
|
||||
*/
|
||||
private array $sampleVersions = [
|
||||
[
|
||||
'version' => '1.0.0',
|
||||
'changes_summary' => [
|
||||
'added' => 0,
|
||||
'removed' => 0,
|
||||
'is_restored' => false,
|
||||
'restored_from' => null,
|
||||
],
|
||||
],
|
||||
[
|
||||
'version' => '1.1.0',
|
||||
'changes_summary' => [
|
||||
'added' => 10,
|
||||
'removed' => 5,
|
||||
'is_restored' => false,
|
||||
'restored_from' => null,
|
||||
],
|
||||
],
|
||||
[
|
||||
'version' => '2.0.0',
|
||||
'changes_summary' => [
|
||||
'added' => 25,
|
||||
'removed' => 15,
|
||||
'is_restored' => false,
|
||||
'restored_from' => null,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('템플릿 레이아웃 버전 시더 생성을 시작합니다.');
|
||||
|
||||
// 기존 버전 데이터 삭제
|
||||
$this->deleteExistingVersions();
|
||||
|
||||
// 레이아웃 조회
|
||||
$layouts = TemplateLayout::all();
|
||||
|
||||
if ($layouts->isEmpty()) {
|
||||
$this->command->warn('레이아웃이 없습니다. TemplateSeeder를 먼저 실행해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 관리자 사용자 조회
|
||||
$adminUser = User::where('is_super', true)->first();
|
||||
|
||||
if (!$adminUser) {
|
||||
$this->command->warn('관리자 사용자가 없습니다. AdminUserSeeder를 먼저 실행해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 각 레이아웃에 대해 버전 생성
|
||||
foreach ($layouts as $layout) {
|
||||
$this->createVersionsForLayout($layout, $adminUser);
|
||||
}
|
||||
|
||||
$this->command->info('템플릿 레이아웃 버전 시더가 성공적으로 생성되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 버전 데이터 삭제
|
||||
*/
|
||||
private function deleteExistingVersions(): void
|
||||
{
|
||||
$deletedCount = TemplateLayoutVersion::query()->delete();
|
||||
|
||||
if ($deletedCount > 0) {
|
||||
$this->command->warn("기존 레이아웃 버전 {$deletedCount}건을 삭제했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 레이아웃에 대한 버전 생성
|
||||
*/
|
||||
private function createVersionsForLayout(TemplateLayout $layout, User $adminUser): void
|
||||
{
|
||||
foreach ($this->sampleVersions as $index => $versionData) {
|
||||
// 레이아웃 content를 복사하여 버전별로 약간 수정
|
||||
$content = $layout->content;
|
||||
$content['version'] = $versionData['version'];
|
||||
|
||||
// 버전별로 컴포넌트 수정 시뮬레이션
|
||||
if ($index > 0) {
|
||||
$content['meta']['description'] = "Version {$versionData['version']} - Updated layout";
|
||||
}
|
||||
|
||||
$version = TemplateLayoutVersion::create([
|
||||
'layout_id' => $layout->id,
|
||||
'version' => $versionData['version'],
|
||||
'content' => $content,
|
||||
'changes_summary' => $versionData['changes_summary'],
|
||||
'created_by' => $adminUser->id,
|
||||
]);
|
||||
|
||||
$this->command->info("레이아웃 '{$layout->name}'의 버전 {$version->version}이 생성되었습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Sample;
|
||||
|
||||
use App\Models\Template;
|
||||
use App\Models\TemplateLayout;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class TemplateSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 기본 템플릿 정보
|
||||
*/
|
||||
private array $defaultTemplate = [
|
||||
'identifier' => 'sirsoft-admin_basic',
|
||||
'vendor' => 'sirsoft',
|
||||
'name' => [
|
||||
'ko' => 'Admin Basic',
|
||||
'en' => 'Admin Basic',
|
||||
],
|
||||
'version' => '0.1.1',
|
||||
'type' => 'admin',
|
||||
'description' => [
|
||||
'ko' => '그누보드7 기본 관리자 템플릿',
|
||||
'en' => 'Gnuboard7 default admin template',
|
||||
],
|
||||
'status' => 'inactive',
|
||||
];
|
||||
|
||||
/**
|
||||
* 기본 레이아웃 정보
|
||||
*/
|
||||
private array $defaultLayout = [
|
||||
'name' => '_admin_base',
|
||||
'content' => [
|
||||
'version' => '0.1.1',
|
||||
'layout_name' => '_admin_base',
|
||||
'meta' => [
|
||||
'title' => '$t:common.admin',
|
||||
'description' => 'Admin base layout',
|
||||
],
|
||||
'components' => [
|
||||
[
|
||||
'id' => 'root',
|
||||
'type' => 'layout',
|
||||
'name' => 'Container',
|
||||
'props' => [
|
||||
'layout' => 'flex',
|
||||
'direction' => 'column',
|
||||
],
|
||||
'children' => [
|
||||
[
|
||||
'id' => 'header',
|
||||
'type' => 'composite',
|
||||
'name' => 'AdminHeader',
|
||||
'props' => [],
|
||||
],
|
||||
[
|
||||
'id' => 'content',
|
||||
'type' => 'layout',
|
||||
'name' => 'Slot',
|
||||
'props' => [
|
||||
'name' => 'content',
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'footer',
|
||||
'type' => 'composite',
|
||||
'name' => 'AdminFooter',
|
||||
'props' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('템플릿 시더 생성을 시작합니다.');
|
||||
|
||||
// 기존 데이터 삭제
|
||||
$this->deleteExistingTemplates();
|
||||
|
||||
// 기본 템플릿 생성
|
||||
$template = $this->createDefaultTemplate();
|
||||
|
||||
// 기본 레이아웃 생성
|
||||
$this->createTemplateLayouts($template);
|
||||
|
||||
$this->command->info('템플릿 시더가 성공적으로 생성되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 템플릿 삭제
|
||||
*/
|
||||
private function deleteExistingTemplates(): void
|
||||
{
|
||||
$deletedCount = Template::where('identifier', $this->defaultTemplate['identifier'])->forceDelete();
|
||||
|
||||
if ($deletedCount > 0) {
|
||||
$this->command->warn("기존 템플릿 {$deletedCount}건을 삭제했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 템플릿 생성
|
||||
*/
|
||||
private function createDefaultTemplate(): Template
|
||||
{
|
||||
// JSON 구조 검증
|
||||
if (! is_array($this->defaultTemplate['name']) || ! is_array($this->defaultTemplate['description'])) {
|
||||
throw new \Exception('Template name and description must be arrays');
|
||||
}
|
||||
|
||||
$template = Template::create($this->defaultTemplate);
|
||||
|
||||
$this->command->info("템플릿이 생성되었습니다: {$template->identifier}");
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 레이아웃 생성
|
||||
*/
|
||||
private function createTemplateLayouts(Template $template): void
|
||||
{
|
||||
$layout = TemplateLayout::create([
|
||||
'template_id' => $template->id,
|
||||
'name' => $this->defaultLayout['name'],
|
||||
'content' => $this->defaultLayout['content'],
|
||||
]);
|
||||
|
||||
$this->command->info("레이아웃이 생성되었습니다: {$layout->name}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user