> */ private array $colsCache = []; public function run(): void { // ===== 0) 테이블 존재 체크 ===== foreach (['admin_roles', 'admin_permissions', 'admin_permission_role', 'admin_users', 'admin_role_user'] as $t) { if (!Schema::hasTable($t)) { throw new \RuntimeException("Missing table: {$t}"); } } // ===== 1) 역할(roles) ===== // 현재 스키마가 name/label 기반(코드 없음)인 것에 맞춰 유지 $roles = [ ['name' => 'super_admin', 'label' => '최고관리자'], ['name' => 'finance', 'label' => '정산관리'], ['name' => 'product', 'label' => '상품관리'], ['name' => 'support', 'label' => 'CS/상담'], ]; foreach ($roles as $r) { $payload = ['label' => $r['label']]; $this->putTs($payload, 'admin_roles'); DB::table('admin_roles')->updateOrInsert( ['name' => $r['name']], $payload ); } // ===== 2) 권한(permissions) ===== $perms = [ ['name' => 'admin.access', 'label' => '관리자 접근'], ['name' => 'settlement.manage', 'label' => '정산 관리'], ['name' => 'product.manage', 'label' => '상품 관리'], ['name' => 'support.manage', 'label' => 'CS/상담 관리'], ['name' => 'member.manage', 'label' => '회원 관리'], ]; foreach ($perms as $p) { $payload = ['label' => $p['label']]; $this->putTs($payload, 'admin_permissions'); DB::table('admin_permissions')->updateOrInsert( ['name' => $p['name']], $payload ); } // ===== 3) 역할별 권한 매핑 ===== // super_admin = 전체 권한 // 일반 role = admin.access + 해당 도메인 권한 $roleId = fn(string $name): int => (int) DB::table('admin_roles')->where('name', $name)->value('id'); $permId = fn(string $name): int => (int) DB::table('admin_permissions')->where('name', $name)->value('id'); $superRoleId = $roleId('super_admin'); $financeRoleId = $roleId('finance'); $productRoleId = $roleId('product'); $supportRoleId = $roleId('support'); $allPermIds = DB::table('admin_permissions')->pluck('id')->map(fn($v) => (int)$v)->all(); foreach ($allPermIds as $pid) { $this->upsertPivot('admin_permission_role', [ 'admin_permission_id' => $pid, 'admin_role_id' => $superRoleId, ]); } // 일반 역할 권한 $this->upsertPivot('admin_permission_role', [ 'admin_permission_id' => $permId('admin.access'), 'admin_role_id' => $financeRoleId, ]); $this->upsertPivot('admin_permission_role', [ 'admin_permission_id' => $permId('settlement.manage'), 'admin_role_id' => $financeRoleId, ]); $this->upsertPivot('admin_permission_role', [ 'admin_permission_id' => $permId('admin.access'), 'admin_role_id' => $productRoleId, ]); $this->upsertPivot('admin_permission_role', [ 'admin_permission_id' => $permId('product.manage'), 'admin_role_id' => $productRoleId, ]); $this->upsertPivot('admin_permission_role', [ 'admin_permission_id' => $permId('admin.access'), 'admin_role_id' => $supportRoleId, ]); $this->upsertPivot('admin_permission_role', [ 'admin_permission_id' => $permId('support.manage'), 'admin_role_id' => $supportRoleId, ]); // ===== 4) super_admin 유저 1명 보장(기존 로직 유지) ===== $seedEmail = (string) env('ADMIN_SEED_EMAIL', 'sungro815@syye.net'); $seedPw = (string) env('ADMIN_SEED_PASSWORD', 'tjekdfl1324%^'); $seedName = (string) env('ADMIN_SEED_NAME', 'Super Admin'); $seedPhone = (string) env('ADMIN_SEED_PHONE', '01036828958'); $this->ensureUserWithRole( email: $seedEmail, passwordPlain: $seedPw, name: $seedName, nickname: '최고관리자', phoneRaw: $seedPhone, roleId: $superRoleId, // super_admin은 phone_hash를 써도 되지만, 스키마/정책 따라 충돌 피하려면 null도 OK usePhoneHash: true ); // ===== 5) 일반 관리자 10명 생성 ===== $fixedPw = 'tjekdfl1324%^'; $fixedPhone = '01036828958'; $koreanNames = [ '김민준', '이서연', '박지훈', '최유진', '정현우', '한지민', '강다은', '조성훈', '윤하늘', '오지후', ]; // 역할 배치: 1~3 finance / 4~6 product / 7~10 support for ($i = 1; $i <= 10; $i++) { $email = "admin{$i}@mail.com"; $name = $koreanNames[$i - 1]; if ($i <= 3) { $rid = $financeRoleId; $nick = "정산담당{$i}"; } elseif ($i <= 6) { $rid = $productRoleId; $nick = "상품담당".($i - 3); } else { $rid = $supportRoleId; $nick = "CS담당".($i - 6); } // 일반 관리자들은 동일 전화번호 사용 → phone_hash 충돌 방지 위해 usePhoneHash=false(=NULL 저장) $this->ensureUserWithRole( email: $email, passwordPlain: $fixedPw, name: $name, nickname: $nick, phoneRaw: $fixedPhone, roleId: $rid, usePhoneHash: false ); } } /** * 유저 생성/갱신 + 역할 부여(스키마 컬럼/피벗 컬럼이 달라도 안전하게) */ private function ensureUserWithRole( string $email, string $passwordPlain, string $name, string $nickname, string $phoneRaw, int $roleId, bool $usePhoneHash ): void { $hashKey = (string) config('admin.phone_hash_key', env('ADMIN_PHONE_HASH_KEY', '')); $phoneDigits = preg_replace('/\D+/', '', $phoneRaw) ?? ''; $phoneEnc = $phoneDigits !== '' ? Crypt::encryptString($phoneDigits) : null; $last4 = $phoneDigits !== '' ? substr($phoneDigits, -4) : null; $hashKey = (string) config('admin.phone_hash_key', env('ADMIN_PHONE_HASH_KEY', '')); if ($hashKey === '') { throw new \RuntimeException('ADMIN_PHONE_HASH_KEY (admin.phone_hash_key) is empty. Set it in .env'); } $phoneDigits = preg_replace('/\D+/', '', $phoneRaw) ?? ''; if ($phoneDigits === '') { throw new \RuntimeException('Seed phone is empty'); } /** * ✅ phone_hash는 NOT NULL이므로 항상 채운다. * - 운영 정책용(=진짜 조회키)일 때: phoneDigits만 * - seed에서 같은 번호 10명 만들 때: phoneDigits + email로 유니크하게 */ $hashInput = $usePhoneHash ? $phoneDigits : ($phoneDigits . '|seed|' . $email); $phoneHash = hash_hmac('sha256', $hashInput, $hashKey); $exists = DB::table('admin_users')->where('email', $email)->first(); if (!$exists) { $insert = []; $this->putIfCol($insert, 'admin_users', 'email', $email); $this->putIfCol($insert, 'admin_users', 'password', Hash::make($passwordPlain)); $this->putIfCol($insert, 'admin_users', 'name', $name); $this->putIfCol($insert, 'admin_users', 'nickname', $nickname); $this->putIfCol($insert, 'admin_users', 'phone_enc', $phoneEnc); $this->putIfCol($insert, 'admin_users', 'phone_hash', $phoneHash); // 일반 관리자는 NULL $this->putIfCol($insert, 'admin_users', 'phone_last4', $last4); $this->putIfCol($insert, 'admin_users', 'status', 'active'); $this->putIfCol($insert, 'admin_users', 'must_reset_password', 0); $this->putIfCol($insert, 'admin_users', 'two_factor_mode', 'sms'); $this->putIfCol($insert, 'admin_users', 'totp_enabled', 0); $this->putIfCol($insert, 'admin_users', 'failed_login_count', 0); $this->putIfCol($insert, 'admin_users', 'locked_until', null); $this->putIfCol($insert, 'admin_users', 'remember_token', null); $this->putIfCol($insert, 'admin_users', 'deleted_at', null); $this->putTs($insert, 'admin_users'); $adminUserId = DB::table('admin_users')->insertGetId($insert); } else { $adminUserId = (int) $exists->id; // 이미 있으면 갱신(비번/이름/닉/폰) $update = []; $this->putIfCol($update, 'admin_users', 'password', Hash::make($passwordPlain)); $this->putIfCol($update, 'admin_users', 'name', $name); $this->putIfCol($update, 'admin_users', 'nickname', $nickname); $this->putIfCol($update, 'admin_users', 'phone_enc', $phoneEnc); $this->putIfCol($update, 'admin_users', 'phone_hash', $phoneHash); $this->putIfCol($update, 'admin_users', 'phone_last4', $last4); $this->putIfCol($update, 'admin_users', 'status', 'active'); $this->putTs($update, 'admin_users', onlyUpdated: true); if (!empty($update)) { DB::table('admin_users')->where('id', $adminUserId)->update($update); } } // 역할 부여 $this->upsertPivot('admin_role_user', [ 'admin_user_id' => $adminUserId, 'admin_role_id' => $roleId, ]); } /** * Pivot upsert (assigned_at/created_at/updated_at 등 존재하면 자동 채움) */ private function upsertPivot(string $table, array $keys): void { $values = []; // pivot에 assigned_at 같은 NOT NULL이 있을 수 있어 자동 세팅 if ($this->hasCol($table, 'assigned_at')) $values['assigned_at'] = now(); if ($this->hasCol($table, 'assigned_by')) $values['assigned_by'] = null; if ($this->hasCol($table, 'created_at')) $values['created_at'] = now(); if ($this->hasCol($table, 'updated_at')) $values['updated_at'] = now(); DB::table($table)->updateOrInsert($keys, $values); } private function putTs(array &$payload, string $table, bool $onlyUpdated = false): void { if ($this->hasCol($table, 'updated_at')) $payload['updated_at'] = now(); if (!$onlyUpdated && $this->hasCol($table, 'created_at')) $payload['created_at'] = now(); } private function putIfCol(array &$payload, string $table, string $col, mixed $value): void { if ($this->hasCol($table, $col)) { $payload[$col] = $value; } } private function hasCol(string $table, string $col): bool { if (!isset($this->colsCache[$table])) { $this->colsCache[$table] = Schema::getColumnListing($table); } return in_array($col, $this->colsCache[$table], true); } }