$data 정책 데이터 (key/scope/target/purpose 등) * @return IdentityPolicy 동기화된 정책 */ public function syncPolicy(array $data): IdentityPolicy { return IdentityPolicy::syncOrCreateFromUpgrade( ['key' => $data['key']], [ 'scope' => $data['scope'] ?? 'route', 'target' => $data['target'] ?? $data['key'], 'purpose' => $data['purpose'] ?? 'sensitive_action', 'provider_id' => $data['provider_id'] ?? null, 'grace_minutes' => (int) ($data['grace_minutes'] ?? 0), 'enabled' => (bool) ($data['enabled'] ?? true), 'priority' => (int) ($data['priority'] ?? 100), 'conditions' => $data['conditions'] ?? null, 'source_type' => $data['source_type'] ?? 'core', 'source_identifier' => $data['source_identifier'] ?? 'core', 'applies_to' => $data['applies_to'] ?? 'both', 'fail_mode' => $data['fail_mode'] ?? 'block', ] ); } /** * seed/정의에 없는 stale 정책을 삭제합니다 (완전 동기화 원칙). * * 운영자가 S1d 에서 직접 생성한 정책(source_type='admin')은 영향받지 않습니다. * * @param string $sourceType 확장 타입 (core|module|plugin) * @param string $sourceIdentifier 확장 식별자 * @param array $currentKeys 현재 유효한 정책 key 목록 * @return int 삭제된 정책 수 */ public function cleanupStalePolicies( string $sourceType, string $sourceIdentifier, array $currentKeys, ): int { $query = IdentityPolicy::query() ->where('source_type', $sourceType) ->where('source_identifier', $sourceIdentifier); if (! empty($currentKeys)) { $query->whereNotIn('key', $currentKeys); } $targets = $query->get(['id', 'key']); foreach ($targets as $policy) { $policy->delete(); } $count = $targets->count(); if ($count > 0) { Log::info('IdentityPolicySyncHelper: stale 정책 정리', [ 'source_type' => $sourceType, 'source_identifier' => $sourceIdentifier, 'deleted_count' => $count, 'deleted_keys' => $targets->pluck('key')->all(), ]); } return $count; } }