feat(mng): 사용자 역할/부서 매핑 및 부서관리 복구/영구삭제 기능 추가
사용자 관리: - 사용자 등록/수정 시 테넌트별 역할/부서 선택 기능 추가 - Department, UserRole, DepartmentUser 모델 추가 - User 모델에 역할/부서 관계 및 헬퍼 메서드 추가 - syncRoles/syncDepartments 메서드 (forceDelete로 유니크 키 충돌 방지) - 체크박스 UI로 다중 선택 지원 부서 관리: - Soft Delete 필터 (정상만/전체/삭제된 항목만) - 복구(restore) 및 영구삭제(forceDelete) 기능 추가 - Department 모델에 SoftDeletes 트레이트 추가 - 삭제된 항목 빨간 배경 + "삭제됨" 배지 표시
This commit is contained in:
@@ -154,4 +154,58 @@ public function destroy(Request $request, int $id): JsonResponse
|
||||
'message' => '부서가 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 복원
|
||||
*/
|
||||
public function restore(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$result = $this->departmentService->restoreDepartment($id);
|
||||
|
||||
if (! $result) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '부서 복원에 실패했습니다.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '부서가 복원되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '부서가 복원되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 영구 삭제
|
||||
*/
|
||||
public function forceDelete(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$result = $this->departmentService->forceDeleteDepartment($id);
|
||||
|
||||
if (! $result) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '부서 영구 삭제에 실패했습니다. (하위 부서가 존재할 수 있습니다)',
|
||||
], 400);
|
||||
}
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '부서가 영구 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '부서가 영구 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\Role;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -25,7 +27,13 @@ public function index(Request $request): View
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('users.create');
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 역할/부서 목록 (테넌트별)
|
||||
$roles = $tenantId ? Role::where('tenant_id', $tenantId)->orderBy('name')->get() : collect();
|
||||
$departments = $tenantId ? Department::where('tenant_id', $tenantId)->where('is_active', true)->orderBy('name')->get() : collect();
|
||||
|
||||
return view('users.create', compact('roles', 'departments'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,6 +47,16 @@ public function edit(int $id): View
|
||||
abort(404, '사용자를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
return view('users.edit', compact('user'));
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 역할/부서 목록 (테넌트별)
|
||||
$roles = $tenantId ? Role::where('tenant_id', $tenantId)->orderBy('name')->get() : collect();
|
||||
$departments = $tenantId ? Department::where('tenant_id', $tenantId)->where('is_active', true)->orderBy('name')->get() : collect();
|
||||
|
||||
// 사용자의 현재 역할/부서 ID 목록
|
||||
$userRoleIds = $tenantId ? $user->userRoles()->where('tenant_id', $tenantId)->pluck('role_id')->toArray() : [];
|
||||
$userDepartmentIds = $tenantId ? $user->departmentUsers()->where('tenant_id', $tenantId)->pluck('department_id')->toArray() : [];
|
||||
|
||||
return view('users.edit', compact('user', 'roles', 'departments', 'userRoleIds', 'userDepartmentIds'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ public function rules(): array
|
||||
'role' => 'nullable|string|max:50',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'is_super_admin' => 'nullable|boolean',
|
||||
'role_ids' => 'nullable|array',
|
||||
'role_ids.*' => 'integer|exists:roles,id',
|
||||
'department_ids' => 'nullable|array',
|
||||
'department_ids.*' => 'integer|exists:departments,id',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@ public function rules(): array
|
||||
'role' => 'nullable|string|max:50',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'is_super_admin' => 'nullable|boolean',
|
||||
'role_ids' => 'nullable|array',
|
||||
'role_ids.*' => 'integer|exists:roles,id',
|
||||
'department_ids' => 'nullable|array',
|
||||
'department_ids.*' => 'integer|exists:departments,id',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
74
app/Models/Department.php
Normal file
74
app/Models/Department.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Department extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'departments';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'parent_id',
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tenant_id' => 'integer',
|
||||
'parent_id' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'deleted_by',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 상위 부서
|
||||
*/
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 부서
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서-사용자 매핑 (DepartmentUser pivot)
|
||||
*/
|
||||
public function departmentUsers(): HasMany
|
||||
{
|
||||
return $this->hasMany(DepartmentUser::class, 'department_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서에 속한 사용자들 (belongsToMany)
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'department_user')
|
||||
->withTimestamps()
|
||||
->withPivot(['tenant_id', 'is_primary', 'joined_at', 'left_at']);
|
||||
}
|
||||
}
|
||||
51
app/Models/DepartmentUser.php
Normal file
51
app/Models/DepartmentUser.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class DepartmentUser extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'department_user';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'department_id',
|
||||
'user_id',
|
||||
'is_primary',
|
||||
'joined_at',
|
||||
'left_at',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tenant_id' => 'integer',
|
||||
'department_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'is_primary' => 'boolean',
|
||||
'joined_at' => 'datetime',
|
||||
'left_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 부서
|
||||
*/
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'department_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,13 @@
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Permission\Traits\HasPermissions;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class Department extends Model
|
||||
{
|
||||
use HasPermissions, HasRoles, ModelTrait; // 부서도 권한/역할을 가짐
|
||||
use HasPermissions, HasRoles, ModelTrait, SoftDeletes; // 부서도 권한/역할을 가짐
|
||||
|
||||
protected $table = 'departments';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
@@ -86,4 +87,44 @@ public function currentTenant()
|
||||
|
||||
return $this->tenants()->find($tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계: 사용자-역할 (user_roles 테이블, 테넌트별)
|
||||
*/
|
||||
public function userRoles(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserRole::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계: 사용자-부서 (department_user 테이블, 테넌트별)
|
||||
*/
|
||||
public function departmentUsers(): HasMany
|
||||
{
|
||||
return $this->hasMany(DepartmentUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테넌트의 역할 목록 조회
|
||||
*/
|
||||
public function getRolesForTenant(int $tenantId)
|
||||
{
|
||||
return $this->userRoles()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('role')
|
||||
->get()
|
||||
->pluck('role');
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테넌트의 부서 목록 조회
|
||||
*/
|
||||
public function getDepartmentsForTenant(int $tenantId)
|
||||
{
|
||||
return $this->departmentUsers()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('department')
|
||||
->get()
|
||||
->pluck('department');
|
||||
}
|
||||
}
|
||||
|
||||
44
app/Models/UserRole.php
Normal file
44
app/Models/UserRole.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class UserRole extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'user_roles';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'tenant_id',
|
||||
'role_id',
|
||||
'assigned_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'tenant_id' => 'integer',
|
||||
'role_id' => 'integer',
|
||||
'assigned_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 사용자
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할
|
||||
*/
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,22 @@ public function getDepartments(array $filters = [], int $perPage = 15): LengthAw
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$query = Department::query()->with('parent');
|
||||
$query = Department::query()->withTrashed()->with('parent');
|
||||
|
||||
// Tenant 필터링 (선택된 경우에만)
|
||||
if ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
// Soft Delete 필터
|
||||
if (isset($filters['trashed'])) {
|
||||
if ($filters['trashed'] === 'only') {
|
||||
$query->onlyTrashed();
|
||||
} elseif ($filters['trashed'] === 'with') {
|
||||
$query->withTrashed();
|
||||
}
|
||||
}
|
||||
|
||||
// 검색 필터
|
||||
if (! empty($filters['search'])) {
|
||||
$search = $filters['search'];
|
||||
@@ -134,6 +143,55 @@ public function deleteDepartment(int $id): bool
|
||||
return $department->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 복원
|
||||
*/
|
||||
public function restoreDepartment(int $id): bool
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$query = Department::onlyTrashed();
|
||||
|
||||
if ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
$department = $query->find($id);
|
||||
|
||||
if (! $department) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $department->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 영구 삭제
|
||||
*/
|
||||
public function forceDeleteDepartment(int $id): bool
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$query = Department::withTrashed();
|
||||
|
||||
if ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
$department = $query->find($id);
|
||||
|
||||
if (! $department) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 하위 부서가 있는 경우 삭제 불가
|
||||
if ($department->children()->count() > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $department->forceDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 코드 중복 체크
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\DepartmentUser;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
@@ -87,6 +89,13 @@ public function createUser(array $data): User
|
||||
'is_default' => true,
|
||||
'joined_at' => now(),
|
||||
]);
|
||||
|
||||
// 역할/부서 동기화
|
||||
$roleIds = $data['role_ids'] ?? [];
|
||||
$departmentIds = $data['department_ids'] ?? [];
|
||||
|
||||
$this->syncRoles($user, $tenantId, $roleIds);
|
||||
$this->syncDepartments($user, $tenantId, $departmentIds);
|
||||
}
|
||||
|
||||
return $user;
|
||||
@@ -102,6 +111,8 @@ public function updateUser(int $id, array $data): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 비밀번호가 입력된 경우만 업데이트
|
||||
if (! empty($data['password'])) {
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
@@ -115,9 +126,67 @@ public function updateUser(int $id, array $data): bool
|
||||
// 수정자 정보
|
||||
$data['updated_by'] = auth()->id();
|
||||
|
||||
// 역할/부서 동기화 (테넌트가 선택된 경우)
|
||||
if ($tenantId) {
|
||||
$roleIds = $data['role_ids'] ?? [];
|
||||
$departmentIds = $data['department_ids'] ?? [];
|
||||
|
||||
$this->syncRoles($user, $tenantId, $roleIds);
|
||||
$this->syncDepartments($user, $tenantId, $departmentIds);
|
||||
}
|
||||
|
||||
// role_ids, department_ids는 User 모델의 fillable이 아니므로 제거
|
||||
unset($data['role_ids'], $data['department_ids']);
|
||||
|
||||
return $user->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 역할 동기화 (특정 테넌트)
|
||||
*/
|
||||
public function syncRoles(User $user, int $tenantId, array $roleIds): void
|
||||
{
|
||||
// 기존 역할 삭제 (해당 테넌트만) - forceDelete로 실제 삭제
|
||||
UserRole::withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->where('tenant_id', $tenantId)
|
||||
->forceDelete();
|
||||
|
||||
// 새 역할 추가
|
||||
foreach ($roleIds as $roleId) {
|
||||
UserRole::create([
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenantId,
|
||||
'role_id' => $roleId,
|
||||
'assigned_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 부서 동기화 (특정 테넌트)
|
||||
*/
|
||||
public function syncDepartments(User $user, int $tenantId, array $departmentIds): void
|
||||
{
|
||||
// 기존 부서 삭제 (해당 테넌트만) - forceDelete로 실제 삭제
|
||||
DepartmentUser::withTrashed()
|
||||
->where('user_id', $user->id)
|
||||
->where('tenant_id', $tenantId)
|
||||
->forceDelete();
|
||||
|
||||
// 새 부서 추가 (첫 번째를 primary로)
|
||||
foreach ($departmentIds as $index => $departmentId) {
|
||||
DepartmentUser::create([
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenantId,
|
||||
'department_id' => $departmentId,
|
||||
'is_primary' => $index === 0,
|
||||
'joined_at' => now(),
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 삭제 (Soft Delete)
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
|
||||
<!-- 활성 상태 필터 -->
|
||||
<div class="w-48">
|
||||
<div class="w-40">
|
||||
<select name="is_active" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="1">활성</option>
|
||||
@@ -31,6 +31,15 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 삭제 상태 필터 -->
|
||||
<div class="w-40">
|
||||
<select name="trashed" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">정상만</option>
|
||||
<option value="with">전체 (삭제 포함)</option>
|
||||
<option value="only">삭제된 항목만</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 검색 버튼 -->
|
||||
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition">
|
||||
검색
|
||||
@@ -85,5 +94,35 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 복원 확인
|
||||
window.confirmRestore = function(id, name) {
|
||||
if (confirm(`"${name}" 부서를 복원하시겠습니까?`)) {
|
||||
htmx.ajax('POST', `/api/admin/departments/${id}/restore`, {
|
||||
target: '#department-table',
|
||||
swap: 'none',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
}).then(() => {
|
||||
htmx.trigger('#department-table', 'filterSubmit');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 영구 삭제 확인
|
||||
window.confirmForceDelete = function(id, name) {
|
||||
if (confirm(`"${name}" 부서를 영구 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다!`)) {
|
||||
htmx.ajax('DELETE', `/api/admin/departments/${id}/force`, {
|
||||
target: '#department-table',
|
||||
swap: 'none',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
}).then(() => {
|
||||
htmx.trigger('#department-table', 'filterSubmit');
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
@@ -11,21 +11,25 @@
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($departments as $department)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<tr class="{{ $department->trashed() ? 'bg-red-50' : 'hover:bg-gray-50' }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm {{ $department->trashed() ? 'text-gray-400' : 'text-gray-900' }}">
|
||||
{{ $department->code }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $department->name }}</div>
|
||||
<div class="text-sm font-medium {{ $department->trashed() ? 'text-gray-400' : 'text-gray-900' }}">{{ $department->name }}</div>
|
||||
@if($department->description)
|
||||
<div class="text-sm text-gray-500">{{ Str::limit($department->description, 50) }}</div>
|
||||
<div class="text-sm {{ $department->trashed() ? 'text-gray-300' : 'text-gray-500' }}">{{ Str::limit($department->description, 50) }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm {{ $department->trashed() ? 'text-gray-400' : 'text-gray-500' }}">
|
||||
{{ $department->parent?->name ?? '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($department->is_active)
|
||||
@if($department->trashed())
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
삭제됨
|
||||
</span>
|
||||
@elseif($department->is_active)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
활성
|
||||
</span>
|
||||
@@ -35,16 +39,25 @@
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm {{ $department->trashed() ? 'text-gray-400' : 'text-gray-500' }}">
|
||||
{{ $department->sort_order }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<a href="{{ route('departments.edit', $department->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">
|
||||
수정
|
||||
</a>
|
||||
<button onclick="confirmDelete({{ $department->id }}, '{{ $department->name }}')" class="text-red-600 hover:text-red-900">
|
||||
삭제
|
||||
</button>
|
||||
@if($department->trashed())
|
||||
<button onclick="confirmRestore({{ $department->id }}, '{{ $department->name }}')" class="text-green-600 hover:text-green-900 mr-3">
|
||||
복원
|
||||
</button>
|
||||
<button onclick="confirmForceDelete({{ $department->id }}, '{{ $department->name }}')" class="text-red-600 hover:text-red-900">
|
||||
영구삭제
|
||||
</button>
|
||||
@else
|
||||
<a href="{{ route('departments.edit', $department->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">
|
||||
수정
|
||||
</a>
|
||||
<button onclick="confirmDelete({{ $department->id }}, '{{ $department->name }}')" class="text-red-600 hover:text-red-900">
|
||||
삭제
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
|
||||
@@ -90,28 +90,59 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 권한 설정 -->
|
||||
<!-- 역할 설정 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">권한 설정</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">역할</label>
|
||||
<input type="text" name="role" maxlength="50"
|
||||
placeholder="예: 관리자, 사용자"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">역할 설정</h2>
|
||||
@if($roles->isNotEmpty())
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
@foreach($roles as $role)
|
||||
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer">
|
||||
<input type="checkbox" name="role_ids[]" value="{{ $role->id }}"
|
||||
{{ in_array($role->id, old('role_ids', [])) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">{{ $role->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_active" value="1" checked
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">활성 상태</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_super_admin" value="1"
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">슈퍼 관리자</span>
|
||||
</label>
|
||||
@else
|
||||
<p class="text-sm text-gray-500">선택 가능한 역할이 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- 부서 설정 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">부서 설정</h2>
|
||||
@if($departments->isNotEmpty())
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
@foreach($departments as $department)
|
||||
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer">
|
||||
<input type="checkbox" name="department_ids[]" value="{{ $department->id }}"
|
||||
{{ in_array($department->id, old('department_ids', [])) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-green-600 rounded focus:ring-2 focus:ring-green-500">
|
||||
<span class="ml-2 text-sm text-gray-700">{{ $department->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">첫 번째로 선택한 부서가 주 부서로 설정됩니다.</p>
|
||||
@else
|
||||
<p class="text-sm text-gray-500">선택 가능한 부서가 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- 계정 상태 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">계정 상태</h2>
|
||||
<div class="flex items-center gap-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_active" value="1" checked
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">활성 상태</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_super_admin" value="1"
|
||||
class="h-4 w-4 text-red-600 rounded focus:ring-2 focus:ring-red-500">
|
||||
<span class="ml-2 text-sm text-gray-700">슈퍼 관리자</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -94,31 +94,61 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 권한 설정 -->
|
||||
<!-- 역할 설정 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">권한 설정</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">역할</label>
|
||||
<input type="text" name="role" maxlength="50"
|
||||
value="{{ old('role', $user->role) }}"
|
||||
placeholder="예: 관리자, 사용자"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">역할 설정</h2>
|
||||
@if($roles->isNotEmpty())
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
@foreach($roles as $role)
|
||||
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer">
|
||||
<input type="checkbox" name="role_ids[]" value="{{ $role->id }}"
|
||||
{{ in_array($role->id, old('role_ids', $userRoleIds)) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">{{ $role->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_active" value="1"
|
||||
{{ old('is_active', $user->is_active) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">활성 상태</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_super_admin" value="1"
|
||||
{{ old('is_super_admin', $user->is_super_admin) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">슈퍼 관리자</span>
|
||||
</label>
|
||||
@else
|
||||
<p class="text-sm text-gray-500">선택 가능한 역할이 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- 부서 설정 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">부서 설정</h2>
|
||||
@if($departments->isNotEmpty())
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
@foreach($departments as $department)
|
||||
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer">
|
||||
<input type="checkbox" name="department_ids[]" value="{{ $department->id }}"
|
||||
{{ in_array($department->id, old('department_ids', $userDepartmentIds)) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-green-600 rounded focus:ring-2 focus:ring-green-500">
|
||||
<span class="ml-2 text-sm text-gray-700">{{ $department->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">첫 번째로 선택한 부서가 주 부서로 설정됩니다.</p>
|
||||
@else
|
||||
<p class="text-sm text-gray-500">선택 가능한 부서가 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- 계정 상태 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">계정 상태</h2>
|
||||
<div class="flex items-center gap-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_active" value="1"
|
||||
{{ old('is_active', $user->is_active) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">활성 상태</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="is_super_admin" value="1"
|
||||
{{ old('is_super_admin', $user->is_super_admin) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-red-600 rounded focus:ring-2 focus:ring-red-500">
|
||||
<span class="ml-2 text-sm text-gray-700">슈퍼 관리자</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
Route::get('/{id}', [DepartmentController::class, 'show'])->name('show');
|
||||
Route::put('/{id}', [DepartmentController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [DepartmentController::class, 'destroy'])->name('destroy');
|
||||
Route::post('/{id}/restore', [DepartmentController::class, 'restore'])->name('restore');
|
||||
Route::delete('/{id}/force', [DepartmentController::class, 'forceDelete'])->name('forceDelete');
|
||||
});
|
||||
|
||||
// 사용자 관리 API
|
||||
@@ -113,4 +115,13 @@
|
||||
Route::post('/deny-all', [\App\Http\Controllers\Api\Admin\DepartmentPermissionController::class, 'denyAll'])->name('denyAll');
|
||||
Route::post('/reset', [\App\Http\Controllers\Api\Admin\DepartmentPermissionController::class, 'reset'])->name('reset');
|
||||
});
|
||||
|
||||
// 개인 권한 관리 API
|
||||
Route::prefix('user-permissions')->name('user-permissions.')->group(function () {
|
||||
Route::get('/matrix', [\App\Http\Controllers\Api\Admin\UserPermissionController::class, 'getMatrix'])->name('matrix');
|
||||
Route::post('/toggle', [\App\Http\Controllers\Api\Admin\UserPermissionController::class, 'toggle'])->name('toggle');
|
||||
Route::post('/allow-all', [\App\Http\Controllers\Api\Admin\UserPermissionController::class, 'allowAll'])->name('allowAll');
|
||||
Route::post('/deny-all', [\App\Http\Controllers\Api\Admin\UserPermissionController::class, 'denyAll'])->name('denyAll');
|
||||
Route::post('/reset', [\App\Http\Controllers\Api\Admin\UserPermissionController::class, 'reset'])->name('reset');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user