style: 관리자 패널 UI 개선 및 스크럼 모달 통합
- 테이블 헤더 스타일 통일 (menus, roles, permissions, boards 등) - 권한 매트릭스 체크박스/버튼 크기 20x20으로 표준화 - 스크럼 항목 추가/수정 모달 통합 (코드 중복 제거) - daily-logs API URL 경로 수정 (/pm/ 제거) - 타임존 Asia/Seoul로 변경 - flow-tester 액션 아이콘 크기 조정
This commit is contained in:
@@ -286,6 +286,11 @@ private function getUsersByTenant(?int $tenantId): Collection
|
||||
{
|
||||
$query = User::where('is_active', true);
|
||||
|
||||
// 일반 관리자는 슈퍼관리자를 볼 수 없음
|
||||
if (! auth()->user()?->is_super_admin) {
|
||||
$query->where('is_super_admin', false);
|
||||
}
|
||||
|
||||
if ($tenantId) {
|
||||
$query->whereHas('tenants', function ($q) use ($tenantId) {
|
||||
$q->where('tenants.id', $tenantId)
|
||||
@@ -358,9 +363,10 @@ private function getUserRoles(int $userId, ?int $tenantId): array
|
||||
public function traceUsersWithPermission(int $menuId, string $permissionType = 'view', ?int $tenantId = null, string $guardName = 'api'): array
|
||||
{
|
||||
$permissionName = "menu:{$menuId}.{$permissionType}";
|
||||
$excludeSuperAdmin = ! auth()->user()?->is_super_admin;
|
||||
|
||||
// 역할로 권한이 있는 사용자
|
||||
$usersFromRole = DB::table('model_has_roles as mhr')
|
||||
$usersFromRoleQuery = DB::table('model_has_roles as mhr')
|
||||
->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id')
|
||||
->join('permissions as p', 'p.id', '=', 'rhp.permission_id')
|
||||
->join('users as u', 'u.id', '=', 'mhr.model_id')
|
||||
@@ -369,12 +375,18 @@ public function traceUsersWithPermission(int $menuId, string $permissionType = '
|
||||
->where('mhr.model_type', User::class)
|
||||
->where('p.guard_name', $guardName)
|
||||
->where('p.name', $permissionName)
|
||||
->where('u.is_active', true)
|
||||
->get();
|
||||
->where('u.is_active', true);
|
||||
|
||||
// 일반 관리자는 슈퍼관리자를 볼 수 없음
|
||||
if ($excludeSuperAdmin) {
|
||||
$usersFromRoleQuery->where('u.is_super_admin', false);
|
||||
}
|
||||
|
||||
$usersFromRole = $usersFromRoleQuery->get();
|
||||
|
||||
// 부서로 권한이 있는 사용자
|
||||
$now = now();
|
||||
$usersFromDepartment = DB::table('department_user as du')
|
||||
$usersFromDepartmentQuery = DB::table('department_user as du')
|
||||
->join('permission_overrides as po', function ($j) use ($now) {
|
||||
$j->on('po.model_id', '=', 'du.department_id')
|
||||
->where('po.model_type', Department::class)
|
||||
@@ -396,15 +408,19 @@ public function traceUsersWithPermission(int $menuId, string $permissionType = '
|
||||
->where('p.name', $permissionName)
|
||||
->where('u.is_active', true);
|
||||
|
||||
if ($excludeSuperAdmin) {
|
||||
$usersFromDepartmentQuery->where('u.is_super_admin', false);
|
||||
}
|
||||
|
||||
if ($tenantId) {
|
||||
$usersFromDepartment->where('du.tenant_id', $tenantId)
|
||||
$usersFromDepartmentQuery->where('du.tenant_id', $tenantId)
|
||||
->where('po.tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
$usersFromDepartment = $usersFromDepartment->get();
|
||||
$usersFromDepartment = $usersFromDepartmentQuery->get();
|
||||
|
||||
// 개인 ALLOW 오버라이드가 있는 사용자
|
||||
$usersFromPersonal = DB::table('permission_overrides as po')
|
||||
$usersFromPersonalQuery = DB::table('permission_overrides as po')
|
||||
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
||||
->join('users as u', 'u.id', '=', 'po.model_id')
|
||||
->select('u.id as user_id', 'u.name as user_name', 'u.email', 'po.effect')
|
||||
@@ -421,14 +437,18 @@ public function traceUsersWithPermission(int $menuId, string $permissionType = '
|
||||
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
||||
});
|
||||
|
||||
if ($tenantId) {
|
||||
$usersFromPersonal->where('po.tenant_id', $tenantId);
|
||||
if ($excludeSuperAdmin) {
|
||||
$usersFromPersonalQuery->where('u.is_super_admin', false);
|
||||
}
|
||||
|
||||
$usersFromPersonal = $usersFromPersonal->get();
|
||||
if ($tenantId) {
|
||||
$usersFromPersonalQuery->where('po.tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
$usersFromPersonal = $usersFromPersonalQuery->get();
|
||||
|
||||
// 개인 DENY 오버라이드가 있는 사용자
|
||||
$usersWithDeny = DB::table('permission_overrides as po')
|
||||
$usersWithDenyQuery = DB::table('permission_overrides as po')
|
||||
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
||||
->join('users as u', 'u.id', '=', 'po.model_id')
|
||||
->select('u.id as user_id', 'u.name as user_name', 'u.email')
|
||||
@@ -445,11 +465,15 @@ public function traceUsersWithPermission(int $menuId, string $permissionType = '
|
||||
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
||||
});
|
||||
|
||||
if ($tenantId) {
|
||||
$usersWithDeny->where('po.tenant_id', $tenantId);
|
||||
if ($excludeSuperAdmin) {
|
||||
$usersWithDenyQuery->where('u.is_super_admin', false);
|
||||
}
|
||||
|
||||
$usersWithDeny = $usersWithDeny->get();
|
||||
if ($tenantId) {
|
||||
$usersWithDenyQuery->where('po.tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
$usersWithDeny = $usersWithDenyQuery->get();
|
||||
|
||||
return [
|
||||
'by_role' => $usersFromRole->map(function ($item) {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => env('APP_TIMEZONE', 'Asia/Seoul'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 60px;">ID</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">작업 설명</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 150px;">대상 테넌트</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 180px;">대상 정보</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 100px;">레코드 타입</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">레코드 수</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 100px;">삭제자</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 140px;">삭제일시</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 60px;">작업</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">ID</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">작업 설명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">대상 테넌트</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">대상 정보</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">레코드 타입</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">레코드 수</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">삭제자</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">삭제일시</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">코드</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">게시판명</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">유형</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">필드 수</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">생성일</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">액션</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">ID</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">코드</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">게시판명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">유형</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">필드 수</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">상태</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">생성일</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 60px;">순번</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">메뉴명</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">URL</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">순서</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">조회</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">생성</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">수정</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">삭제</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">승인</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 80px;">내보내기</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">관리</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">순번</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">메뉴명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">URL</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">순서</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">조회</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">생성</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">수정</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">삭제</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">승인</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">내보내기</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -24,11 +24,11 @@
|
||||
data-menu-id="{{ $menu->id }}"
|
||||
data-parent-id="{{ $menu->parent_id ?? '' }}"
|
||||
data-depth="{{ $menu->depth ?? 0 }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
{{ $index + 1 }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<div class="flex items-center gap-1.5" style="padding-left: {{ (($menu->depth ?? 0) * 1.25) }}rem;">
|
||||
{{-- 트리 구조 표시 --}}
|
||||
@if(($menu->depth ?? 0) > 0)
|
||||
<span class="text-gray-300 text-xs font-mono flex-shrink-0">└─</span>
|
||||
@@ -59,20 +59,20 @@ class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outl
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
<span class="truncate max-w-xs inline-block" title="{{ $menu->url }}">
|
||||
{{ $menu->url }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
{{ $menu->sort_order }}
|
||||
</td>
|
||||
@foreach($permissionTypes as $type)
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
{{ isset($permissions[$menu->id][$type]) && $permissions[$menu->id][$type] ? 'checked' : '' }}
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||
class="h-5 w-5 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||
hx-post="/api/admin/department-permissions/toggle"
|
||||
hx-trigger="click"
|
||||
hx-target="#permission-matrix"
|
||||
|
||||
@@ -78,12 +78,12 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">이름</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">카테고리</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">스텝</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">최근 실행</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">액션</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">이름</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">카테고리</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">스텝</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">최근 실행</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">상태</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@@ -139,7 +139,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<button onclick="runFlow({{ $flow->id }})"
|
||||
class="p-2 text-green-600 hover:bg-green-50 rounded-lg transition"
|
||||
title="실행">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
@@ -148,7 +148,7 @@ class="p-2 text-green-600 hover:bg-green-50 rounded-lg transition"
|
||||
<a href="{{ route('dev-tools.flow-tester.edit', $flow->id) }}"
|
||||
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition"
|
||||
title="편집">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</a>
|
||||
@@ -156,7 +156,7 @@ class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition"
|
||||
<a href="{{ route('dev-tools.flow-tester.history', $flow->id) }}"
|
||||
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition"
|
||||
title="실행 이력">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
@@ -166,7 +166,7 @@ class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition"
|
||||
<button type="submit"
|
||||
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition"
|
||||
title="복제">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -175,7 +175,7 @@ class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition"
|
||||
<button onclick="confirmDelete({{ $flow->id }}, '{{ $flow->name }}')"
|
||||
class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition"
|
||||
title="삭제">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -449,7 +449,7 @@ function runFlow(id) {
|
||||
const btn = document.querySelector(`button[onclick="runFlow(${id})"]`);
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
btn.innerHTML = `<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>`;
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<table class="w-full">
|
||||
<thead class="bg-purple-50 border-b">
|
||||
<tr>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-10"></th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider w-12">No.</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">메뉴명</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">URL</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider w-14">정렬</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-14">활성</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-14">숨김</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-24">작업</th>
|
||||
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider w-10"></th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">No.</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">메뉴명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">URL</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">정렬</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">활성</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">숨김</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="menu-sortable" class="bg-white divide-y divide-gray-200">
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
<tr>
|
||||
{{-- 체크박스 (가져오기 모드일 때만 표시) --}}
|
||||
@if($importMode ?? false)
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-10">
|
||||
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider w-10">
|
||||
<input type="checkbox"
|
||||
id="selectAllImport"
|
||||
onchange="toggleSelectAllImport(this)"
|
||||
class="w-4 h-4 rounded border-gray-300 text-green-600 focus:ring-green-500">
|
||||
</th>
|
||||
@else
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-10"></th>
|
||||
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider w-10"></th>
|
||||
@endif
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider w-12">No.</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">메뉴명</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">URL</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider w-14">정렬</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-14">활성</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-14">숨김</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-16">구분</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider w-24">작업</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">No.</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">메뉴명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">URL</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">정렬</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">활성</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">숨김</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">구분</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="menu-sortable" class="bg-white divide-y divide-gray-200">
|
||||
|
||||
@@ -67,8 +67,8 @@ class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring
|
||||
|
||||
<!-- 2단 레이아웃 (고정 높이, 좌우 분할) -->
|
||||
<div class="flex gap-6" style="height: calc(100vh - 220px);">
|
||||
<!-- 좌측: 메뉴 트리 (고정 너비) -->
|
||||
<div class="flex-shrink-0" style="width: 320px;">
|
||||
<!-- 좌측: 메뉴 트리 (50%) -->
|
||||
<div class="w-1/2">
|
||||
<div class="bg-white rounded-lg shadow-sm h-full flex flex-col">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-3">메뉴 트리</h2>
|
||||
@@ -87,9 +87,9 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 분석 결과 (나머지 공간) -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="bg-white rounded-lg shadow-sm h-full flex flex-col">
|
||||
<!-- 우측: 분석 결과 (50%) -->
|
||||
<div class="w-1/2 min-w-0">
|
||||
<div class="bg-white rounded-lg shadow-sm h-full flex flex-col w-full">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-800" id="selectedMenuTitle">선택된 메뉴</h2>
|
||||
</div>
|
||||
|
||||
@@ -105,15 +105,15 @@ function getPermissionConfig(string $type): array
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">ID</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">권한명</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">테넌트</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">가드</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">할당된 역할</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">할당된 부서</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">생성일</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">수정일</th>
|
||||
<th class="px-4 py-2 text-right text-sm font-semibold text-gray-700 uppercase tracking-wider">액션</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">ID</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">권한명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">테넌트</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">가드</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">할당된 역할</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">할당된 부서</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">생성일</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">수정일</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
</h4>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button"
|
||||
onclick="openQuickScrumModal({{ $project->id }}, '{{ $project->name }}', {{ $todayScrum?->id ?? 'null' }})"
|
||||
onclick="openAddEntryModal({{ $project->id }}, '{{ $project->name }}', {{ $todayScrum?->id ?? 'null' }})"
|
||||
class="text-xs text-indigo-600 hover:text-indigo-800 flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
@@ -379,7 +379,7 @@ class="text-xs text-gray-500 hover:text-gray-700">
|
||||
<div class="text-center py-4 bg-gray-50 rounded-lg">
|
||||
<p class="text-sm text-gray-400">오늘의 활동이 없습니다</p>
|
||||
<button type="button"
|
||||
onclick="openQuickScrumModal({{ $project->id }}, '{{ $project->name }}', null)"
|
||||
onclick="openAddEntryModal({{ $project->id }}, '{{ $project->name }}', null)"
|
||||
class="mt-2 text-xs text-indigo-600 hover:text-indigo-800">
|
||||
+ 첫 활동 추가하기
|
||||
</button>
|
||||
@@ -452,10 +452,10 @@ class="p-6">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 flex items-center gap-2" id="edit-modal-title">
|
||||
<svg class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg id="editModalIcon" class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
스크럼 항목 수정
|
||||
<span id="editModalTitleText">스크럼 항목 수정</span>
|
||||
</h3>
|
||||
<button type="button" onclick="closeEditEntryModal()" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -471,6 +471,8 @@ class="p-6">
|
||||
|
||||
<form id="editEntryForm" onsubmit="submitEditEntries(event)">
|
||||
<input type="hidden" id="editEntryProjectId" name="project_id">
|
||||
<input type="hidden" id="editEntryLogId" name="log_id">
|
||||
<input type="hidden" id="editEntryIsAddMode" value="false">
|
||||
|
||||
<!-- 담당자 (공통) -->
|
||||
<div class="mb-4">
|
||||
@@ -511,111 +513,6 @@ class="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 빠른 스크럼 추가 모달 -->
|
||||
<div id="quickScrumModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!-- 배경 오버레이 -->
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="closeQuickScrumModal()"></div>
|
||||
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||
|
||||
<!-- 모달 패널 -->
|
||||
<div class="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 flex items-center gap-2" id="modal-title">
|
||||
<svg class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
스크럼 항목 추가
|
||||
</h3>
|
||||
<button type="button" onclick="closeQuickScrumModal()" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 프로젝트 정보 -->
|
||||
<div class="mb-4 p-3 bg-gray-50 rounded-lg">
|
||||
<span class="text-xs text-gray-500">프로젝트</span>
|
||||
<p id="quickScrumProjectName" class="font-medium text-gray-900"></p>
|
||||
</div>
|
||||
|
||||
<form id="quickScrumForm" onsubmit="submitQuickScrum(event)">
|
||||
<input type="hidden" id="quickScrumProjectId" name="project_id">
|
||||
<input type="hidden" id="quickScrumLogId" name="log_id">
|
||||
|
||||
<!-- 담당자 -->
|
||||
<div class="mb-4">
|
||||
<label for="quickScrumAssignee" class="block text-sm font-medium text-gray-700 mb-1">담당자</label>
|
||||
<input type="text"
|
||||
id="quickScrumAssignee"
|
||||
name="assignee_name"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="담당자 이름">
|
||||
</div>
|
||||
|
||||
<!-- 업무 내용 -->
|
||||
<div class="mb-4">
|
||||
<label for="quickScrumContent" class="block text-sm font-medium text-gray-700 mb-1">업무 내용</label>
|
||||
<textarea id="quickScrumContent"
|
||||
name="content"
|
||||
required
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="오늘 진행한/진행할 업무 내용"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 상태 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">상태</label>
|
||||
<div class="flex gap-3">
|
||||
<label class="flex-1 cursor-pointer">
|
||||
<input type="radio" name="status" value="todo" class="sr-only peer" checked>
|
||||
<div class="flex items-center justify-center gap-1 py-2 px-3 border rounded-lg peer-checked:border-gray-500 peer-checked:bg-gray-50 text-sm">
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
|
||||
예정
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex-1 cursor-pointer">
|
||||
<input type="radio" name="status" value="in_progress" class="sr-only peer">
|
||||
<div class="flex items-center justify-center gap-1 py-2 px-3 border rounded-lg peer-checked:border-blue-500 peer-checked:bg-blue-50 text-sm">
|
||||
<span class="w-2 h-2 bg-blue-500 rounded-full"></span>
|
||||
진행중
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex-1 cursor-pointer">
|
||||
<input type="radio" name="status" value="done" class="sr-only peer">
|
||||
<div class="flex items-center justify-center gap-1 py-2 px-3 border rounded-lg peer-checked:border-green-500 peer-checked:bg-green-50 text-sm">
|
||||
<svg class="w-3.5 h-3.5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
완료
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex gap-3">
|
||||
<button type="button"
|
||||
onclick="closeQuickScrumModal()"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">
|
||||
취소
|
||||
</button>
|
||||
<button type="submit"
|
||||
id="quickScrumSubmitBtn"
|
||||
class="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@@ -731,118 +628,47 @@ function renderOpenIssues(container, issues) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 빠른 스크럼 모달 관련 함수
|
||||
function openQuickScrumModal(projectId, projectName, logId) {
|
||||
document.getElementById('quickScrumProjectId').value = projectId;
|
||||
document.getElementById('quickScrumProjectName').textContent = projectName;
|
||||
document.getElementById('quickScrumLogId').value = logId || '';
|
||||
document.getElementById('quickScrumModal').classList.remove('hidden');
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('quickScrumAssignee').value = '';
|
||||
document.getElementById('quickScrumContent').value = '';
|
||||
document.querySelector('input[name="status"][value="todo"]').checked = true;
|
||||
|
||||
// 담당자 입력창에 포커스
|
||||
setTimeout(() => {
|
||||
document.getElementById('quickScrumAssignee').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function closeQuickScrumModal() {
|
||||
document.getElementById('quickScrumModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function submitQuickScrum(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = document.getElementById('quickScrumForm');
|
||||
const submitBtn = document.getElementById('quickScrumSubmitBtn');
|
||||
const projectId = document.getElementById('quickScrumProjectId').value;
|
||||
const logId = document.getElementById('quickScrumLogId').value;
|
||||
|
||||
const formData = {
|
||||
project_id: parseInt(projectId),
|
||||
log_date: new Date().toISOString().split('T')[0],
|
||||
assignee_name: document.getElementById('quickScrumAssignee').value,
|
||||
content: document.getElementById('quickScrumContent').value,
|
||||
status: document.querySelector('input[name="status"]:checked').value,
|
||||
assignee_type: 'user'
|
||||
};
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '추가 중...';
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (logId) {
|
||||
// 기존 로그에 항목 추가
|
||||
response = await fetch(`/api/admin/pm/daily-logs/${logId}/entries`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
} else {
|
||||
// 새 로그 생성과 함께 항목 추가
|
||||
response = await fetch('/api/admin/pm/daily-logs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({
|
||||
project_id: formData.project_id,
|
||||
log_date: formData.log_date,
|
||||
entries: [{
|
||||
assignee_type: formData.assignee_type,
|
||||
assignee_name: formData.assignee_name,
|
||||
content: formData.content,
|
||||
status: formData.status
|
||||
}]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
closeQuickScrumModal();
|
||||
// 페이지 새로고침으로 데이터 반영
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.message || '스크럼 항목 추가에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('오류가 발생했습니다. 다시 시도해주세요.');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '추가';
|
||||
}
|
||||
}
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
if (!document.getElementById('quickScrumModal').classList.contains('hidden')) {
|
||||
closeQuickScrumModal();
|
||||
}
|
||||
if (!document.getElementById('editEntryModal').classList.contains('hidden')) {
|
||||
closeEditEntryModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 스크럼 항목 수정 모달 관련 함수 (담당자별 그룹 편집)
|
||||
// 스크럼 항목 추가/수정 모달 관련 함수 (담당자별 그룹 편집)
|
||||
let currentEditEntries = []; // 현재 편집 중인 항목들
|
||||
let entriesToDelete = []; // 삭제할 항목 ID 목록
|
||||
|
||||
// 스크럼 항목 추가 모달 열기 (새 항목)
|
||||
function openAddEntryModal(projectId, projectName, logId) {
|
||||
currentEditEntries = [];
|
||||
entriesToDelete = [];
|
||||
|
||||
document.getElementById('editEntryProjectId').value = projectId;
|
||||
document.getElementById('editEntryProjectName').textContent = projectName;
|
||||
document.getElementById('editEntryLogId').value = logId || '';
|
||||
document.getElementById('editEntryIsAddMode').value = 'true';
|
||||
document.getElementById('editEntryAssignee').value = '';
|
||||
|
||||
// 모달 제목 변경 (추가 모드)
|
||||
document.getElementById('editModalTitleText').textContent = '스크럼 항목 추가';
|
||||
const icon = document.getElementById('editModalIcon');
|
||||
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />';
|
||||
|
||||
// 빈 항목 1개로 시작
|
||||
renderEntryRows([{ id: '', content: '', status: 'todo' }]);
|
||||
|
||||
document.getElementById('editEntryModal').classList.remove('hidden');
|
||||
|
||||
// 담당자 입력창에 포커스
|
||||
setTimeout(() => {
|
||||
document.getElementById('editEntryAssignee').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 스크럼 항목 수정 모달 열기 (기존 항목)
|
||||
function openEditEntryModal(entriesJson, assigneeName, projectId, projectName) {
|
||||
const entries = JSON.parse(entriesJson);
|
||||
currentEditEntries = entries;
|
||||
@@ -850,8 +676,15 @@ function openEditEntryModal(entriesJson, assigneeName, projectId, projectName) {
|
||||
|
||||
document.getElementById('editEntryProjectId').value = projectId;
|
||||
document.getElementById('editEntryProjectName').textContent = projectName;
|
||||
document.getElementById('editEntryLogId').value = entries[0]?.daily_log_id || '';
|
||||
document.getElementById('editEntryIsAddMode').value = 'false';
|
||||
document.getElementById('editEntryAssignee').value = assigneeName;
|
||||
|
||||
// 모달 제목 변경 (수정 모드)
|
||||
document.getElementById('editModalTitleText').textContent = '스크럼 항목 수정';
|
||||
const icon = document.getElementById('editModalIcon');
|
||||
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />';
|
||||
|
||||
// 항목 컨테이너 초기화 및 렌더링
|
||||
renderEntryRows(entries);
|
||||
|
||||
@@ -965,6 +798,9 @@ function closeEditEntryModal() {
|
||||
|
||||
const submitBtn = document.getElementById('editEntrySubmitBtn');
|
||||
const assigneeName = document.getElementById('editEntryAssignee').value;
|
||||
const projectId = document.getElementById('editEntryProjectId').value;
|
||||
const logId = document.getElementById('editEntryLogId').value;
|
||||
const isAddMode = document.getElementById('editEntryIsAddMode').value === 'true';
|
||||
const container = document.getElementById('editEntriesContainer');
|
||||
const rows = container.querySelectorAll('.entry-row');
|
||||
|
||||
@@ -972,23 +808,27 @@ function closeEditEntryModal() {
|
||||
submitBtn.textContent = '저장 중...';
|
||||
|
||||
try {
|
||||
const promises = [];
|
||||
// 항목 데이터 수집
|
||||
const newEntries = [];
|
||||
const updatePromises = [];
|
||||
|
||||
// 삭제 처리
|
||||
for (const entryId of entriesToDelete) {
|
||||
promises.push(
|
||||
fetch(`/api/admin/daily-logs/entries/${entryId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
})
|
||||
);
|
||||
// 삭제 처리 (수정 모드에서만)
|
||||
if (!isAddMode) {
|
||||
for (const entryId of entriesToDelete) {
|
||||
updatePromises.push(
|
||||
fetch(`/api/admin/daily-logs/entries/${entryId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 수정/생성 처리
|
||||
rows.forEach((row, index) => {
|
||||
// 항목 처리
|
||||
rows.forEach((row) => {
|
||||
const entryId = row.dataset.entryId;
|
||||
const content = row.querySelector('textarea').value.trim();
|
||||
const status = row.querySelector('input[type="hidden"]').value;
|
||||
@@ -1004,7 +844,7 @@ function closeEditEntryModal() {
|
||||
|
||||
if (entryId) {
|
||||
// 기존 항목 수정
|
||||
promises.push(
|
||||
updatePromises.push(
|
||||
fetch(`/api/admin/daily-logs/entries/${entryId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
@@ -1016,31 +856,76 @@ function closeEditEntryModal() {
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// 새 항목 - 기존 항목의 daily_log_id를 사용
|
||||
const existingEntry = currentEditEntries.find(e => e.id);
|
||||
if (existingEntry && existingEntry.daily_log_id) {
|
||||
promises.push(
|
||||
fetch(`/api/admin/pm/daily-logs/${existingEntry.daily_log_id}/entries`, {
|
||||
// 새 항목
|
||||
newEntries.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
// 새 항목 처리
|
||||
if (newEntries.length > 0) {
|
||||
if (logId) {
|
||||
// 기존 로그에 항목 추가
|
||||
for (const entry of newEntries) {
|
||||
updatePromises.push(
|
||||
fetch(`/api/admin/daily-logs/${logId}/entries`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify(entry)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (isAddMode) {
|
||||
// 새 로그 생성과 함께 항목 추가
|
||||
const response = await fetch('/api/admin/daily-logs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({
|
||||
project_id: parseInt(projectId),
|
||||
log_date: new Date().toISOString().split('T')[0],
|
||||
entries: newEntries
|
||||
})
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || '스크럼 항목 추가에 실패했습니다.');
|
||||
}
|
||||
} else {
|
||||
// 수정 모드에서 새 항목 추가 - 기존 항목의 daily_log_id 사용
|
||||
const existingEntry = currentEditEntries.find(e => e.id);
|
||||
if (existingEntry && existingEntry.daily_log_id) {
|
||||
for (const entry of newEntries) {
|
||||
updatePromises.push(
|
||||
fetch(`/api/admin/daily-logs/${existingEntry.daily_log_id}/entries`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify(entry)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
closeEditEntryModal();
|
||||
window.location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('오류가 발생했습니다. 다시 시도해주세요.');
|
||||
alert(error.message || '오류가 발생했습니다. 다시 시도해주세요.');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '저장';
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('pm.index') }}" class="text-gray-500 hover:text-gray-700">
|
||||
← 대시보드
|
||||
<a href="{{ route('pm.index') }}" class="inline-flex items-center gap-1 px-3 py-1.5 text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
대시보드
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="px-3 py-1 text-sm rounded-full {{ $project->status_color }}">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-3 py-1.5 text-sm font-medium rounded-lg {{ $project->status_color }}">
|
||||
{{ $project->status_label }}
|
||||
</span>
|
||||
<a href="{{ route('pm.projects.edit', $project->id) }}"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-sm">
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg transition text-sm font-medium">
|
||||
수정
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 60px;">순번</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">메뉴명</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">URL</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">순서</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">조회</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">생성</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">수정</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">삭제</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">승인</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 80px;">내보내기</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">관리</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">순번</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">메뉴명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">URL</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">순서</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">조회</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">생성</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">수정</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">삭제</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">승인</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">내보내기</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -24,11 +24,11 @@
|
||||
data-menu-id="{{ $menu->id }}"
|
||||
data-parent-id="{{ $menu->parent_id ?? '' }}"
|
||||
data-depth="{{ $menu->depth ?? 0 }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
{{ $index + 1 }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<div class="flex items-center gap-1.5" style="padding-left: {{ (($menu->depth ?? 0) * 1.25) }}rem;">
|
||||
{{-- 트리 구조 표시 --}}
|
||||
@if(($menu->depth ?? 0) > 0)
|
||||
<span class="text-gray-300 text-xs font-mono flex-shrink-0">└─</span>
|
||||
@@ -59,20 +59,20 @@ class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outl
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
<span class="truncate max-w-xs inline-block" title="{{ $menu->url }}">
|
||||
{{ $menu->url }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
{{ $menu->sort_order }}
|
||||
</td>
|
||||
@foreach($permissionTypes as $type)
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
{{ isset($permissions[$menu->id][$type]) && $permissions[$menu->id][$type] ? 'checked' : '' }}
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||
class="h-5 w-5 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||
hx-post="/api/admin/role-permissions/toggle"
|
||||
hx-target="#permission-matrix"
|
||||
hx-include="[name='role_id'],[name='guard_name']"
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">ID</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">ID</th>
|
||||
@if($isAllTenants)
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">테넌트</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">테넌트</th>
|
||||
@endif
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">역할 이름</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">Guard</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">설명</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">권한 수</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">생성일</th>
|
||||
<th class="px-6 py-3 text-right text-sm font-semibold text-gray-700 uppercase tracking-wider">액션</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">역할 이름</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">Guard</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">설명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">권한 수</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">생성일</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 60px;">순번</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">메뉴명</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">URL</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">순서</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">조회</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">생성</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">수정</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">삭제</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">승인</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 80px;">내보내기</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider" style="width: 70px;">관리</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">순번</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">메뉴명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">URL</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">순서</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">조회</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">생성</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">수정</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">삭제</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">승인</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">내보내기</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -24,11 +24,11 @@
|
||||
data-menu-id="{{ $menu->id }}"
|
||||
data-parent-id="{{ $menu->parent_id ?? '' }}"
|
||||
data-depth="{{ $menu->depth ?? 0 }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
{{ $index + 1 }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<div class="flex items-center gap-1.5" style="padding-left: {{ (($menu->depth ?? 0) * 1.25) }}rem;">
|
||||
{{-- 트리 구조 표시 --}}
|
||||
@if(($menu->depth ?? 0) > 0)
|
||||
<span class="text-gray-300 text-xs font-mono flex-shrink-0">└─</span>
|
||||
@@ -59,12 +59,12 @@ class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outl
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
<span class="truncate max-w-xs inline-block" title="{{ $menu->url }}">
|
||||
{{ $menu->url }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900 text-center">
|
||||
{{ $menu->sort_order }}
|
||||
</td>
|
||||
@foreach($permissionTypes as $type)
|
||||
@@ -103,10 +103,10 @@ class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outl
|
||||
$title = '미설정 (클릭: 개인 허용으로 변경)';
|
||||
}
|
||||
@endphp
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-center">
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-200 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 {{ $bgClass }}"
|
||||
class="w-5 h-5 rounded flex items-center justify-center transition-all duration-200 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 {{ $bgClass }}"
|
||||
title="{{ $title }}"
|
||||
hx-post="/api/admin/user-permissions/toggle"
|
||||
hx-target="#permission-matrix"
|
||||
@@ -115,17 +115,17 @@ class="w-8 h-8 rounded-lg flex items-center justify-center transition-all durati
|
||||
>
|
||||
@if($icon === 'allow')
|
||||
{{-- 허용: 체크 아이콘 --}}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
@elseif($icon === 'deny')
|
||||
{{-- 거부: X 아이콘 --}}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
@else
|
||||
{{-- 미설정: 마이너스 아이콘 --}}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"></path>
|
||||
</svg>
|
||||
@endif
|
||||
@@ -145,43 +145,43 @@ class="w-8 h-8 rounded-lg flex items-center justify-center transition-all durati
|
||||
</div>
|
||||
|
||||
{{-- 범례 --}}
|
||||
<div class="mt-4 flex flex-wrap items-center gap-6 text-sm text-gray-600 px-6 pb-4">
|
||||
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600 px-3 pb-2">
|
||||
<span class="font-medium">범례:</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded bg-gray-100 text-gray-400">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-gray-100 text-gray-400">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span>미설정</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded bg-purple-100 text-purple-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-purple-100 text-purple-600">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span>역할</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded bg-blue-100 text-blue-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-blue-100 text-blue-600">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span>부서</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded bg-green-100 text-green-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-green-100 text-green-600">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span>개인 허용</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded bg-red-100 text-red-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-red-100 text-red-600">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
@@ -181,18 +181,23 @@
|
||||
Route::prefix('dev-tools')->name('dev-tools.')->group(function () {
|
||||
// API 플로우 테스터
|
||||
Route::prefix('flow-tester')->name('flow-tester.')->group(function () {
|
||||
// 고정 경로 먼저 (구체적인 경로)
|
||||
Route::get('/', [FlowTesterController::class, 'index'])->name('index');
|
||||
Route::get('/create', [FlowTesterController::class, 'create'])->name('create');
|
||||
Route::post('/', [FlowTesterController::class, 'store'])->name('store');
|
||||
Route::post('/validate-json', [FlowTesterController::class, 'validateJson'])->name('validate-json');
|
||||
|
||||
// /runs/* 관련 라우트 (고정 경로)
|
||||
Route::get('/runs/{runId}/status', [FlowTesterController::class, 'runStatus'])->name('run-status');
|
||||
Route::get('/runs/{runId}', [FlowTesterController::class, 'runDetail'])->name('run-detail');
|
||||
|
||||
// /{id}/* 관련 라우트 (와일드카드는 마지막에)
|
||||
Route::get('/{id}', [FlowTesterController::class, 'edit'])->name('edit');
|
||||
Route::put('/{id}', [FlowTesterController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [FlowTesterController::class, 'destroy'])->name('destroy');
|
||||
Route::post('/{id}/clone', [FlowTesterController::class, 'clone'])->name('clone');
|
||||
Route::post('/validate-json', [FlowTesterController::class, 'validateJson'])->name('validate-json');
|
||||
Route::post('/{id}/run', [FlowTesterController::class, 'run'])->name('run');
|
||||
Route::get('/runs/{runId}/status', [FlowTesterController::class, 'runStatus'])->name('run-status');
|
||||
Route::get('/{id}/history', [FlowTesterController::class, 'history'])->name('history');
|
||||
Route::get('/runs/{runId}', [FlowTesterController::class, 'runDetail'])->name('run-detail');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user