# 사용자 수정 보존 (HasUserOverrides Trait) > **배경**: 그누보드7 은 업그레이드/시더 재실행 시 사용자가 UI/API 로 수정한 필드를 **자동 보존**하는 시스템을 제공합니다. 본 문서는 `HasUserOverrides` trait 의 사용법과 보장 범위를 정의합니다. ## TL;DR (5초 요약) ```text 1. 모델에 `use HasUserOverrides;` + `protected array $trackableFields = [...]` 선언 2. 다국어 JSON 컬럼은 `protected array $translatableTrackableFields = ['name']` 추가 — sub-key dot-path 단위 보존 3. `user_overrides` (JSON/array) 컬럼 마이그레이션 필수 4. 사용자가 trackable 필드를 수정 → user_overrides 자동 누적 기록 (다국어는 'name.ko' 형태) 5. 시더가 `syncOrCreateFromUpgrade()` 호출 → 기록된 키만 보존, 나머지 locale 은 자동 동기화 6. mass update `Model::where(...)->update(...)` 도 **투명하게 자동 추적** 7. 언어팩 활성/비활성 시 `RunSeedersOnLanguagePackLifecycle` 가 entity 시더 자동 재실행 (scope 별 라우팅) ``` ## 1. 기본 사용법 ### 1.1 모델 선언 ```php use App\Models\Concerns\HasUserOverrides; use Illuminate\Database\Eloquent\Model; class ShippingType extends Model { use HasUserOverrides; /** * 사용자 수정 보존 대상 필드 (전체 컬럼 단위). * * @var array */ protected array $trackableFields = ['name', 'category', 'is_active', 'sort_order']; /** * 다국어 JSON 컬럼 — sub-key dot-path 단위로 user_overrides 보존. * * 사용자가 ko 라벨만 수정 → user_overrides=['name.ko'] 로 기록되어 * 시더 재실행/언어팩 활성 시 ja/en 키는 자동 동기화 가능. * * 정책: trackableFields 의 부분집합. 일반 scalar 필드(category 등)는 등록 금지. * * @var array */ protected array $translatableTrackableFields = ['name']; protected $fillable = [ 'code', 'name', 'category', 'is_active', 'sort_order', 'user_overrides', // 필수 ]; protected $casts = [ 'name' => 'array', 'is_active' => 'boolean', 'sort_order' => 'integer', 'user_overrides' => 'array', // 필수 (JSON ↔ array 자동 변환) ]; } ``` ### 1.2 다국어 JSON 컬럼 정책 (translatableTrackableFields) 7.0.0-beta.4 이후 다국어 JSON 컬럼은 sub-key dot-path 단위로 user_overrides 가 기록됩니다. | 시나리오 | user_overrides 기록 형식 | |---------|-------------------------| | 사용자가 `name.ko` 만 수정 | `['name.ko']` | | 사용자가 `name.ko, name.en` 동시 수정 | `['name.ko', 'name.en']` | | 사용자가 scalar 필드 `is_active` 수정 | `['is_active']` (컬럼명 단위) | | 혼재 수정 | `['name.ko', 'is_active']` | 시더가 재실행될 때: | 상황 | 동작 | |------|------| | `user_overrides=['name.ko']` 인 row + 시더가 `{ko, en, ja}` 신규 데이터 제공 | ko 보존, en/ja 신규 값으로 갱신 | | `user_overrides=['is_active']` 인 row + 시더가 is_active=true 제공 | 갱신 SKIP (기존 동작) | | `user_overrides=['name']` legacy 컬럼명 형식 | 컬럼 전체 보존 (역호환 — beta.4 upgrade step 이 dot-path 로 자동 변환) | ### 1.2 마이그레이션 ```php Schema::table('ecommerce_shipping_types', function (Blueprint $table) { $table->text('user_overrides')->nullable() ->comment('유저가 수정한 필드명 목록 (예: ["name", "category"])'); }); ``` ### 1.3 Seeder (업그레이드 경로) ```php public function run(): void { $helper = app(\App\Extension\Helpers\GenericEntitySyncHelper::class); $definedCodes = []; foreach ($types as $data) { $helper->sync(ShippingType::class, ['code' => $data['code']], $data); $definedCodes[] = $data['code']; } // 완전 동기화: seeder 에 없는 row 삭제 (user_overrides 무관) $helper->cleanupStale(ShippingType::class, [], 'code', $definedCodes); } ``` ## 2. 동작 메커니즘 ### 2.1 사용자 수정 기록 (자동) 사용자가 수정할 때 trait 의 `updating` 이벤트 핸들러가 자동으로 변경된 trackable 필드를 `user_overrides` 배열에 추가합니다. ```php $shippingType = ShippingType::find(1); $shippingType->name = ['ko' => '사용자 수정']; $shippingType->save(); // → user_overrides = ['name'] 자동 기록 ``` ### 2.2 시더 재실행 시 보존 ```php // 시더/업그레이드에서 호출 $helper->sync(ShippingType::class, ['code' => 'parcel'], [ 'name' => ['ko' => '택배'], // 사용자가 수정했으므로 건너뜀 'sort_order' => 1, // user_overrides 에 없으므로 갱신 ]); ``` `syncOrCreateFromUpgrade()` 내부는: - 기존 row 가 있으면 `user_overrides` 에 등록된 trait 필드는 갱신 건너뜀 - 나머지 필드는 갱신 - 시더 컨텍스트(`user_overrides.seeding` 플래그) 에서는 `updating` 이벤트가 자동 bypass 되어 user_overrides 가 추가 기록되지 않음 ## 3. Mass Update 투명 추적 ### 3.1 지원 경로 다음 수정 경로 모두에서 user_overrides 가 자동 기록됩니다: | 경로 | 예시 | 기록 여부 | |---|---|---| | 인스턴스 update | `$model->update([...])` | ✅ 기록 | | 인스턴스 save | `$model->name = 'x'; $model->save()` | ✅ 기록 | | **mass update** | `Model::where(...)->update([...])` | ✅ **기록** | | `whereIn` mass update | `Model::whereIn('id', [...])->update([...])` | ✅ 기록 (행별 계산) | | Query Builder 우회 | `DB::table('...')->update(...)` | ❌ 기록 안 됨 (escape hatch) | ### 3.2 내부 구현 `HasUserOverrides` trait 이 커스텀 `UserOverridesAwareBuilder` 를 반환하여 mass update 를 가로챕니다: 1. 시더 컨텍스트면 기본 경로 유지 2. 입력 `$values` 에 trackable 필드가 없으면 기본 경로 유지 (성능 최적화) 3. 그 외: 대상 행 preload → 행별 `calculateUserOverridesFor($values)` 계산 → user_overrides 포함한 per-row UPDATE ### 3.3 성능 고려 | 상황 | 쿼리 수 | |---|---| | trackable 필드 미포함 update | 1 UPDATE (기존) | | trackable 필드 포함 mass update | 1 SELECT + N UPDATE | | 대량 처리 필요 (수천~수만 행) | **`DB::table(...)->update(...)` 사용 권장** (Eloquent 우회) | ## 4. 공개 API ### 4.1 `calculateUserOverridesFor(array $incoming): array` 현재 모델 상태와 신규 입력을 비교하여 user_overrides 배열을 계산합니다. ```php $model = ShippingType::find(1); $overrides = $model->calculateUserOverridesFor(['name' => ['ko' => '새 이름']]); // 결과: ['name'] (현재 값과 다른 trackable 필드만 포함) ``` 주로 `UserOverridesAwareBuilder` 가 내부적으로 사용하며, 복잡한 커스텀 sync 로직에서도 재사용 가능합니다. ### 4.2 `syncOrCreateFromUpgrade(array $finder, array $attributes): self` 시더/업그레이드에서 직접 호출하거나, helper 에 위임합니다. ```php ShippingType::syncOrCreateFromUpgrade( ['code' => 'parcel'], ['name' => ['ko' => '택배'], 'category' => 'domestic'] ); ``` ### 4.3 `getTrackableFields(): array` `$trackableFields` 프로퍼티 반환. 커스텀 로직에서 참조. ## 5. 정책과 원칙 ### 5.1 row 존재 여부 vs 필드 값 | 구분 | 정책 | |---|---| | **row 존재** | 오직 config/seeder 정의 기준 (user_overrides 무관) — stale cleanup 은 무조건 삭제 | | **필드 값 (유지되는 row)** | user_overrides 에 등록된 trackable 필드는 보존, 나머지는 갱신 | 즉 "사용자가 수정한 row 이니 보존" 이 아니라 "config 에 있으면 유지, 있으면서 사용자가 수정한 필드는 유지" 입니다. ### 5.2 Trait 적용 시점의 한계 Trait 이 적용되기 **이전에** 사용자가 수정한 row 는 user_overrides 가 비어있어 추적 불가. Trait 적용 이후 수정부터 보존 효과 발생. 해결: 마이그레이션 + trait 적용을 포함한 업그레이드 후 사용자에게 설정 재확인을 안내. ## 6. 적용 현황 | 모델 | 트랙 필드 | |---|---| | `Menu` | `name`, `icon`, `order`, `url` | | `Role` | `name`, `description` | | `NotificationDefinition` | `name`, `is_active` | | `NotificationTemplate` | `subject`, `body`, `click_url`, `recipients`, `is_active` | | `Schedule` | `expression`, `command`, `timeout`, `is_active` | | `BoardType` | `name` | | `ClaimReason` | `name`, `sort_order`, `is_active` | | `ShippingType` | `name`, `category`, `is_active`, `sort_order` | ## 7. 참고 - 완전 동기화 원칙: [core-config.md](core-config.md#완전-동기화-원칙) - Helper 5종 사용 가이드: [data-sync-helpers.md](data-sync-helpers.md) - 업그레이드 스텝: [extension/upgrade-step-guide.md](../extension/upgrade-step-guide.md)