메뉴 관리 HTMX 에러 수정 및 개발도구 메뉴 동적 렌더링
- HTMX 응답 에러 수정: JSON 래핑 대신 HTML 직접 반환 - MenuController, GlobalMenuController의 index 메소드 수정 - index.blade.php, global-index.blade.php의 JSON 파싱 로직 제거 - 메뉴 options 필드 검증 추가 - StoreMenuRequest, UpdateMenuRequest에 options 필드 추가 - section 변경이 정상 저장되도록 수정 - 개발도구 메뉴 하드코딩 제거, DB 기반 동적 렌더링 - sidebar.blade.php에서 하드코딩된 메뉴 제거 - tools-menu.blade.php 컴포넌트 신규 생성 - section=tools 메뉴가 하단 고정 영역에 동적 표시
This commit is contained in:
@@ -18,18 +18,18 @@ public function __construct(
|
||||
/**
|
||||
* 글로벌 메뉴 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse|\Illuminate\Http\Response
|
||||
{
|
||||
$menus = $this->menuService->getGlobalMenus(
|
||||
$request->all(),
|
||||
$request->integer('per_page', 100) // 글로벌 메뉴는 많지 않으므로 페이지당 100개
|
||||
);
|
||||
|
||||
// HTMX 요청인 경우 HTML 반환
|
||||
// HTMX 요청인 경우 HTML 직접 반환 (JSON 래핑 없이)
|
||||
if ($request->header('HX-Request')) {
|
||||
$html = view('menus.partials.global-table', compact('menus'))->render();
|
||||
|
||||
return response()->json(['html' => $html]);
|
||||
return response($html)->header('Content-Type', 'text/html');
|
||||
}
|
||||
|
||||
// 일반 API 요청인 경우 JSON 반환
|
||||
|
||||
@@ -18,7 +18,7 @@ public function __construct(
|
||||
/**
|
||||
* 메뉴 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse|\Illuminate\Http\Response
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$importMode = $request->get('mode') === 'import' && $tenantId;
|
||||
@@ -34,11 +34,11 @@ public function index(Request $request): JsonResponse
|
||||
);
|
||||
}
|
||||
|
||||
// HTMX 요청인 경우 HTML 반환
|
||||
// HTMX 요청인 경우 HTML 직접 반환 (JSON 래핑 없이)
|
||||
if ($request->header('HX-Request')) {
|
||||
$html = view('menus.partials.table', compact('menus', 'importMode'))->render();
|
||||
|
||||
return response()->json(['html' => $html]);
|
||||
return response($html)->header('Content-Type', 'text/html');
|
||||
}
|
||||
|
||||
// 일반 API 요청인 경우 JSON 반환
|
||||
|
||||
@@ -31,6 +31,9 @@ public function rules(): array
|
||||
'hidden' => 'nullable|boolean',
|
||||
'is_external' => 'nullable|boolean',
|
||||
'external_url' => 'nullable|string|max:255|required_if:is_external,1',
|
||||
'options' => 'nullable|array',
|
||||
'options.section' => 'nullable|string|in:main,tools,labs',
|
||||
'options.meta' => 'nullable|array',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ function ($attribute, $value, $fail) use ($menuId) {
|
||||
'hidden' => 'nullable|boolean',
|
||||
'is_external' => 'nullable|boolean',
|
||||
'external_url' => 'nullable|string|max:255|required_if:is_external,1',
|
||||
'options' => 'nullable|array',
|
||||
'options.section' => 'nullable|string|in:main,tools,labs',
|
||||
'options.meta' => 'nullable|array',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
68
resources/views/components/sidebar/tools-menu.blade.php
Normal file
68
resources/views/components/sidebar/tools-menu.blade.php
Normal file
@@ -0,0 +1,68 @@
|
||||
@props(['menus'])
|
||||
|
||||
@php
|
||||
$sidebarMenuService = app(\App\Services\SidebarMenuService::class);
|
||||
@endphp
|
||||
|
||||
@foreach($menus as $toolsGroup)
|
||||
@php
|
||||
$groupId = 'tools-group-' . $toolsGroup->id;
|
||||
$children = $toolsGroup->menuChildren ?? collect();
|
||||
$hasChildren = $children->isNotEmpty();
|
||||
$isExpanded = $sidebarMenuService->isMenuOrChildActive($toolsGroup);
|
||||
@endphp
|
||||
|
||||
{{-- 그룹 헤더 (접기/펼치기 버튼) --}}
|
||||
<button
|
||||
onclick="toggleGroup('{{ $groupId }}')"
|
||||
class="sidebar-group-header w-full flex items-center justify-between px-3 py-2 text-xs font-bold text-gray-600 uppercase tracking-wider hover:bg-gray-100 rounded"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
@if($toolsGroup->icon)
|
||||
<x-sidebar.menu-icon :icon="$toolsGroup->icon" class="w-4 h-4 text-orange-500" />
|
||||
@else
|
||||
<svg class="w-4 h-4 text-orange-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
@endif
|
||||
<span class="sidebar-text">{{ $toolsGroup->name }}</span>
|
||||
</span>
|
||||
<svg
|
||||
id="{{ $groupId }}-icon"
|
||||
class="w-3 h-3 transition-transform sidebar-text"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{-- 하위 메뉴 목록 --}}
|
||||
<ul id="{{ $groupId }}" class="space-y-1 mt-1">
|
||||
@if($hasChildren)
|
||||
@foreach($children as $child)
|
||||
@php
|
||||
$isActive = $sidebarMenuService->isMenuActive($child);
|
||||
$url = $child->is_external ? $child->external_url : $child->url;
|
||||
@endphp
|
||||
<li>
|
||||
<a href="{{ $url }}"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-700 hover:bg-gray-100 {{ $isActive ? 'bg-primary text-white hover:bg-primary' : '' }}"
|
||||
title="{{ $child->name }}"
|
||||
@if($child->is_external) target="_blank" rel="noopener noreferrer" @endif>
|
||||
@if($child->icon)
|
||||
<x-sidebar.menu-icon :icon="$child->icon" class="w-4 h-4 flex-shrink-0" />
|
||||
@else
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
@endif
|
||||
<span class="font-medium sidebar-text">{{ $child->name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
@endif
|
||||
</ul>
|
||||
@endforeach
|
||||
@@ -110,13 +110,11 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
});
|
||||
|
||||
// HTMX 응답 처리 + SortableJS 초기화
|
||||
// 서버가 HTML을 직접 반환하므로 HTMX가 자동으로 swap 처리
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'menu-table') {
|
||||
const response = JSON.parse(event.detail.xhr.response);
|
||||
if (response.html) {
|
||||
event.detail.target.innerHTML = response.html;
|
||||
initGlobalMenuSortable();
|
||||
}
|
||||
// 테이블 로드 후 SortableJS 초기화
|
||||
initGlobalMenuSortable();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -214,14 +214,11 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
});
|
||||
|
||||
// HTMX 응답 처리 + SortableJS 초기화
|
||||
// 서버가 HTML을 직접 반환하므로 HTMX가 자동으로 swap 처리
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'menu-table') {
|
||||
const response = JSON.parse(event.detail.xhr.response);
|
||||
if (response.html) {
|
||||
event.detail.target.innerHTML = response.html;
|
||||
// 테이블 로드 후 SortableJS 초기화
|
||||
initMenuSortable();
|
||||
}
|
||||
// 테이블 로드 후 SortableJS 초기화
|
||||
initMenuSortable();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -39,43 +39,12 @@ class="sidebar-collapsed-only hidden w-full p-2 text-xl font-bold text-gray-900
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 개발 도구 (하단 고정) -->
|
||||
<!-- 개발 도구 (하단 고정, DB 기반) -->
|
||||
@if(!empty($toolsMenus) && $toolsMenus->count() > 0)
|
||||
<div class="border-t border-gray-200 p-2 bg-gray-50">
|
||||
<button onclick="toggleGroup('dev-tools-group')" class="sidebar-group-header w-full flex items-center justify-between px-3 py-2 text-xs font-bold text-gray-600 uppercase tracking-wider hover:bg-gray-100 rounded">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-orange-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span class="sidebar-text">개발 도구</span>
|
||||
</span>
|
||||
<svg id="dev-tools-group-icon" class="w-3 h-3 transition-transform sidebar-text" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul id="dev-tools-group" class="space-y-1 mt-1">
|
||||
<li>
|
||||
<a href="{{ route('dev-tools.flow-tester.index') }}"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-700 hover:bg-gray-100 {{ request()->routeIs('dev-tools.flow-tester.*') ? 'bg-primary text-white hover:bg-primary' : '' }}"
|
||||
title="API 플로우 테스터">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="font-medium sidebar-text">API 플로우 테스터</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route('dev-tools.api-logs.index') }}"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-700 hover:bg-gray-100 {{ request()->routeIs('dev-tools.api-logs.*') ? 'bg-primary text-white hover:bg-primary' : '' }}"
|
||||
title="API 요청 로그">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span class="font-medium sidebar-text">API 요청 로그</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<x-sidebar.tools-menu :menus="$toolsMenus" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user