fix(mng): HTMX SPA 네비게이션 오류 수정

## 수정 내용

### HTMX 응답 형식 수정
- DepartmentController: view 직접 반환 (JSON 래핑 제거)
- MenuController: ->render() 제거하여 SVG 이스케이프 문제 해결

### 사이드바 개선
- hx-boost 적용하여 SPA 스타일 네비게이션 구현
- 메뉴 클릭 시 활성화 상태 즉시 반영
- 스크롤 위치 저장/복원 기능 추가

### 불필요한 코드 제거
- departments/index.blade.php: JSON.parse 코드 제거

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 22:23:03 +09:00
parent cd6cf9746a
commit aa9623c5bb
4 changed files with 64 additions and 30 deletions

View File

@@ -18,20 +18,16 @@ public function __construct(
/**
* 부서 목록 조회
*/
public function index(Request $request): JsonResponse
public function index(Request $request): JsonResponse|\Illuminate\View\View
{
$departments = $this->departmentService->getDepartments(
$request->all(),
$request->integer('per_page', 10)
);
// HTMX 요청 시 HTML 반환
// HTMX 요청 시 HTML 직접 반환
if ($request->header('HX-Request')) {
$html = view('departments.partials.table', compact('departments'))->render();
return response()->json([
'html' => $html,
]);
return view('departments.partials.table', compact('departments'));
}
// 일반 요청 시 JSON 반환

View File

@@ -34,11 +34,9 @@ public function index(Request $request): JsonResponse|\Illuminate\Http\Response
);
}
// HTMX 요청인 경우 HTML 직접 반환 (JSON 래핑 없이)
// HTMX 요청인 경우 HTML 직접 반환
if ($request->header('HX-Request')) {
$html = view('menus.partials.table', compact('menus', 'importMode'))->render();
return response($html)->header('Content-Type', 'text/html');
return view('menus.partials.table', compact('menus', 'importMode'));
}
// 일반 API 요청인 경우 JSON 반환

View File

@@ -70,16 +70,6 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
htmx.trigger('#department-table', 'filterSubmit');
});
// HTMX 응답 처리
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'department-table') {
const response = JSON.parse(event.detail.xhr.response);
if (response.html) {
event.detail.target.innerHTML = response.html;
}
}
});
// 삭제 확인
window.confirmDelete = function(id, name) {
showDeleteConfirm(name, () => {

View File

@@ -63,7 +63,12 @@ class="w-full border-gray-300 rounded-lg text-sm focus:ring-primary focus:border
</div>
<!-- Navigation Menu -->
<nav class="flex-1 overflow-y-auto p-4 sidebar-nav">
<nav class="flex-1 overflow-y-auto p-4 sidebar-nav"
hx-boost="true"
hx-target="#main-content"
hx-select="#main-content"
hx-swap="outerHTML"
hx-push-url="true">
<ul class="space-y-1">
{{-- Main Section Menus (Dynamic from DB) --}}
<x-sidebar.menu-tree :menus="$mainMenus" />
@@ -75,7 +80,12 @@ class="w-full border-gray-300 rounded-lg text-sm focus:ring-primary focus:border
<!-- 개발 도구 (하단 고정, DB 기반) -->
@if(!empty($toolsMenus) && $toolsMenus->count() > 0)
<div class="border-t border-gray-200 p-2 bg-gray-50">
<div class="border-t border-gray-200 p-2 bg-gray-50"
hx-boost="true"
hx-target="#main-content"
hx-select="#main-content"
hx-swap="outerHTML"
hx-push-url="true">
<x-sidebar.tools-menu :menus="$toolsMenus" />
</div>
@endif
@@ -565,20 +575,49 @@ function scrollSidebarToBottom() {
// 저장된 스크롤 위치 복원
function restoreSidebarScroll() {
const sidebarNav = document.querySelector('.sidebar-nav');
const scrollToBottom = localStorage.getItem('sidebar-scroll-bottom');
if (!sidebarNav) return;
if (sidebarNav && scrollToBottom === 'true') {
const scrollToBottom = localStorage.getItem('sidebar-scroll-bottom');
const savedScrollTop = localStorage.getItem('sidebar-scroll-top');
if (scrollToBottom === 'true') {
setTimeout(function() {
sidebarNav.scrollTop = sidebarNav.scrollHeight;
}, 100);
} else if (savedScrollTop) {
setTimeout(function() {
sidebarNav.scrollTop = parseInt(savedScrollTop, 10);
}, 100);
}
}
// 일반 메뉴 클릭 시 스크롤 위치 초기화
function resetSidebarScroll() {
localStorage.removeItem('sidebar-scroll-bottom');
// 메뉴 클릭 시 현재 스크롤 위치 저장
function saveSidebarScroll() {
const sidebarNav = document.querySelector('.sidebar-nav');
if (sidebarNav) {
localStorage.setItem('sidebar-scroll-top', sidebarNav.scrollTop);
localStorage.removeItem('sidebar-scroll-bottom');
}
}
// ========== HTMX 메뉴 활성화 ==========
// 메뉴 클릭 시 활성화 처리
document.getElementById('sidebar').addEventListener('click', function(e) {
const link = e.target.closest('a[href]');
if (!link || link.getAttribute('href') === '#') return;
// 모든 메뉴에서 활성화 클래스 제거
this.querySelectorAll('nav a, .border-t a').forEach(a => {
a.classList.remove('bg-primary', 'text-white', 'hover:bg-primary');
a.classList.add('text-gray-700', 'hover:bg-gray-100');
});
// 클릭한 메뉴에 활성화 클래스 추가
link.classList.remove('text-gray-700', 'hover:bg-gray-100');
link.classList.add('bg-primary', 'text-white', 'hover:bg-primary');
});
// ========== R&D Labs 탭 전환 함수 ==========
// 확장 상태: 탭 전환
@@ -825,13 +864,24 @@ function initSidebarTooltips() {
});
}
// 일반 메뉴 클릭 시 스크롤 위치 초기화
// 메뉴 클릭 시 스크롤 위치 저장
const sidebarNav = document.querySelector('.sidebar-nav');
if (sidebarNav) {
sidebarNav.addEventListener('click', function(e) {
const link = e.target.closest('a[href]');
if (link && !link.closest('#lab-menu-container')) {
resetSidebarScroll();
saveSidebarScroll();
}
});
}
// 개발 도구 메뉴 클릭 시 스크롤 위치 저장
const toolsMenu = document.querySelector('.border-t.border-gray-200');
if (toolsMenu) {
toolsMenu.addEventListener('click', function(e) {
const link = e.target.closest('a[href]');
if (link) {
saveSidebarScroll();
}
});
}