name = '변경 후'; $model->email = 'new@example.com'; $snapshot = [ 'name' => '변경 전', 'email' => 'old@example.com', ]; $changes = ChangeDetector::detect($model, $snapshot); $this->assertNotNull($changes); $this->assertCount(2, $changes); // name 필드 변경 확인 $nameChange = collect($changes)->firstWhere('field', 'name'); $this->assertEquals('model.name', $nameChange['label_key']); $this->assertEquals('변경 전', $nameChange['old']); $this->assertEquals('변경 후', $nameChange['new']); $this->assertEquals('text', $nameChange['type']); // email 필드 변경 확인 $emailChange = collect($changes)->firstWhere('field', 'email'); $this->assertEquals('model.email', $emailChange['label_key']); $this->assertEquals('old@example.com', $emailChange['old']); $this->assertEquals('new@example.com', $emailChange['new']); } /** * 변경이 없으면 null을 반환하는지 확인 */ public function test_returns_null_when_no_changes(): void { $model = new ChangeDetectorTestModel; $model->name = '동일값'; $model->email = 'same@example.com'; $snapshot = [ 'name' => '동일값', 'email' => 'same@example.com', ]; $changes = ChangeDetector::detect($model, $snapshot); $this->assertNull($changes); } /** * snapshot이 null이면 null을 반환하는지 확인 */ public function test_returns_null_when_snapshot_is_null(): void { $model = new ChangeDetectorTestModel; $model->name = 'test'; $changes = ChangeDetector::detect($model, null); $this->assertNull($changes); } /** * enum 타입 필드의 old_label_key와 new_label_key가 포함되는지 확인 */ public function test_handles_enum_type_with_label_keys(): void { $model = new ChangeDetectorEnumTestModel; $model->status = ChangeDetectorTestStatus::Active; $snapshot = [ 'status' => 'inactive', ]; $changes = ChangeDetector::detect($model, $snapshot); $this->assertNotNull($changes); $this->assertCount(1, $changes); $statusChange = $changes[0]; $this->assertEquals('status', $statusChange['field']); $this->assertEquals('model.status', $statusChange['label_key']); $this->assertEquals('enum', $statusChange['type']); $this->assertEquals('inactive', $statusChange['old']); $this->assertEquals('active', $statusChange['new']); $this->assertEquals('test.status.inactive', $statusChange['old_label_key']); $this->assertEquals('test.status.active', $statusChange['new_label_key']); } /** * enum 타입 필드의 old가 null이면 old_label_key도 null인지 확인 */ public function test_enum_null_old_value_returns_null_label_key(): void { $model = new ChangeDetectorEnumTestModel; $model->status = ChangeDetectorTestStatus::Active; $snapshot = [ 'status' => null, ]; $changes = ChangeDetector::detect($model, $snapshot); $this->assertNotNull($changes); $statusChange = $changes[0]; $this->assertNull($statusChange['old_label_key']); $this->assertEquals('test.status.active', $statusChange['new_label_key']); } /** * $activityLogFields에 없는 필드는 무시되는지 확인 */ public function test_ignores_untracked_fields(): void { $model = new ChangeDetectorTestModel; $model->name = '변경됨'; $model->email = 'same@example.com'; $model->untracked_field = '추적 안 됨'; $snapshot = [ 'name' => '원본', 'email' => 'same@example.com', 'untracked_field' => '이전값', ]; $changes = ChangeDetector::detect($model, $snapshot); $this->assertNotNull($changes); $this->assertCount(1, $changes); $this->assertEquals('name', $changes[0]['field']); } /** * $activityLogFields가 없는 모델은 null을 반환하는지 확인 */ public function test_returns_null_when_model_has_no_activity_log_fields(): void { $model = new ChangeDetectorNoFieldsModel; $model->name = 'test'; $snapshot = ['name' => 'old']; $changes = ChangeDetector::detect($model, $snapshot); $this->assertNull($changes); } /** * BackedEnum 객체가 스냅샷에 있을 때 값으로 변환하여 비교하는지 확인 */ public function test_converts_backed_enum_in_snapshot_to_value(): void { $model = new ChangeDetectorEnumTestModel; $model->status = ChangeDetectorTestStatus::Active; // 스냅샷에 Enum 객체가 들어있는 경우 (toArray() 시 발생 가능) $snapshot = [ 'status' => ChangeDetectorTestStatus::Inactive, ]; $changes = ChangeDetector::detect($model, $snapshot); $this->assertNotNull($changes); $this->assertEquals('inactive', $changes[0]['old']); $this->assertEquals('active', $changes[0]['new']); } } // ============================================================ // 테스트용 모델 및 Enum 정의 // ============================================================ /** * 테스트용 모델 (text 필드만) */ class ChangeDetectorTestModel extends Model { /** @var array */ public static array $activityLogFields = [ 'name' => ['label_key' => 'model.name', 'type' => 'text'], 'email' => ['label_key' => 'model.email', 'type' => 'text'], ]; protected $guarded = []; } /** * 테스트용 모델 (enum 필드 포함) */ class ChangeDetectorEnumTestModel extends Model { /** @var array */ public static array $activityLogFields = [ 'status' => [ 'label_key' => 'model.status', 'type' => 'enum', 'enum' => ChangeDetectorTestStatus::class, ], ]; protected $guarded = []; protected function casts(): array { return [ 'status' => ChangeDetectorTestStatus::class, ]; } } /** * 테스트용 모델 ($activityLogFields 없음) */ class ChangeDetectorNoFieldsModel extends Model { protected $guarded = []; } /** * 테스트용 Enum (labelKey() 메서드 포함) */ enum ChangeDetectorTestStatus: string { case Active = 'active'; case Inactive = 'inactive'; /** * 다국어 라벨 키를 반환합니다. * * @return string */ public function labelKey(): string { return 'test.status.'.$this->value; } }