style: 관리자 패널 UI 개선 및 스크럼 모달 통합

- 테이블 헤더 스타일 통일 (menus, roles, permissions, boards 등)
- 권한 매트릭스 체크박스/버튼 크기 20x20으로 표준화
- 스크럼 항목 추가/수정 모달 통합 (코드 중복 제거)
- daily-logs API URL 경로 수정 (/pm/ 제거)
- 타임존 Asia/Seoul로 변경
- flow-tester 액션 아이콘 크기 조정
This commit is contained in:
2025-12-03 16:47:57 +09:00
parent 23c53cbb82
commit 2846d6c034
17 changed files with 316 additions and 399 deletions

View File

@@ -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) {

View File

@@ -65,7 +65,7 @@
|
*/
'timezone' => 'UTC',
'timezone' => env('APP_TIMEZONE', 'Asia/Seoul'),
/*
|--------------------------------------------------------------------------

View File

@@ -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">

View File

@@ -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">

View File

@@ -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"

View File

@@ -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>`;

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">&#8203;</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 = '저장';

View File

@@ -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">

View File

@@ -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>

View File

@@ -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']"

View File

@@ -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">

View File

@@ -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>

View File

@@ -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');
});
});
});