Files
sam-manage/resources/views/layouts/app.blade.php
hskwon 093e98bc0f feat: MNG 모바일 반응형 Phase 1 - 사이드바 오버레이 및 햄버거 메뉴
- 모바일 사이드바 오버레이 구현 (슬라이드 인/아웃)
- 헤더에 햄버거 메뉴 버튼 추가
- 모바일 백드롭 오버레이 추가
- ESC 키 및 메뉴 클릭 시 사이드바 자동 닫힘
2025-12-19 15:51:29 +09:00

306 lines
12 KiB
PHP

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'Dashboard') - {{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<!-- SAM 전역 설정 (FCM 등에서 사용) -->
<script>
window.SAM_CONFIG = {
apiBaseUrl: '{{ config('services.api.base_url', 'https://api.codebridge-x.com') }}',
apiKey: '{{ config('services.api.key', '') }}',
appVersion: '{{ config('app.version', '1.0.0') }}',
};
// API 토큰 sessionStorage 동기화 (FCM 등에서 사용)
@if(session('api_access_token'))
(function() {
const token = '{{ session('api_access_token') }}';
const expiresAt = {{ session('api_token_expires_at', 0) }};
const now = Math.floor(Date.now() / 1000);
// 토큰이 유효한 경우에만 저장
if (expiresAt > now) {
sessionStorage.setItem('api_access_token', token);
sessionStorage.setItem('api_token_expires_at', expiresAt);
} else {
// 만료된 토큰 정리
sessionStorage.removeItem('api_access_token');
sessionStorage.removeItem('api_token_expires_at');
}
})();
@else
// 세션에 토큰이 없으면 sessionStorage도 정리
sessionStorage.removeItem('api_access_token');
sessionStorage.removeItem('api_token_expires_at');
@endif
</script>
<!-- 사이드바 상태 즉시 적용 (깜빡임 방지) -->
<script>
(function() {
if (localStorage.getItem('sidebar-collapsed') === 'true') {
document.documentElement.classList.add('sidebar-is-collapsed');
}
})();
</script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
@stack('styles')
</head>
<body class="bg-gray-100">
<div class="flex h-screen overflow-hidden" id="app-container">
<!-- 모바일 사이드바 백드롭 (lg 미만에서만 동작) -->
<div id="sidebar-backdrop"
class="fixed inset-0 bg-black/50 z-40 hidden lg:hidden"
onclick="closeMobileSidebar()">
</div>
<!-- Sidebar -->
@include('partials.sidebar')
<!-- Main Content Area -->
<div class="flex-1 flex flex-col overflow-hidden min-w-0">
<!-- Header -->
@include('partials.header')
<!-- Page Content -->
<main class="flex-1 overflow-y-auto bg-gray-100 p-6">
@yield('content')
</main>
</div>
</div>
<!-- 전역 컨텍스트 메뉴 -->
@include('components.context-menu')
<!-- 테넌트 정보 모달 -->
@include('components.tenant-modal')
<!-- 사용자 정보 모달 -->
@include('components.user-modal')
<!-- HTMX CSRF 토큰 설정 -->
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
});
</script>
<script src="{{ asset('js/pagination.js') }}"></script>
<script src="{{ asset('js/table-sort.js') }}"></script>
<script src="{{ asset('js/context-menu.js') }}"></script>
<script src="{{ asset('js/tenant-modal.js') }}"></script>
<script src="{{ asset('js/user-modal.js') }}"></script>
<!-- FCM Push Notification (Capacitor 앱에서만 동작) -->
<script src="{{ asset('js/fcm.js') }}"></script>
<!-- SweetAlert2 공통 함수 (Tailwind 테마) -->
<script>
// Tailwind 커스텀 클래스
const SwalTailwind = Swal.mixin({
customClass: {
popup: 'rounded-xl shadow-2xl border-0',
title: 'text-gray-900 font-semibold',
htmlContainer: 'text-gray-600',
confirmButton: 'bg-blue-600 hover:bg-blue-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-blue-300',
cancelButton: 'bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-gray-100',
denyButton: 'bg-red-600 hover:bg-red-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-red-300',
actions: 'gap-3',
},
buttonsStyling: false,
});
// Toast (스낵바) - 우상단 알림
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
customClass: {
popup: 'rounded-lg shadow-lg',
},
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
/**
* 토스트 알림 표시
* @param {string} message - 메시지
* @param {string} type - 'success' | 'error' | 'warning' | 'info'
* @param {number} timer - 표시 시간 (ms), 기본 3000
*/
function showToast(message, type = 'info', timer = 3000) {
Toast.fire({
icon: type,
title: message,
timer: timer,
});
}
/**
* 확인 모달 표시
* @param {string} message - 확인 메시지
* @param {Function} onConfirm - 확인 시 콜백
* @param {Object} options - 추가 옵션
*/
function showConfirm(message, onConfirm, options = {}) {
const defaultOptions = {
title: options.title || '확인',
icon: options.icon || 'question',
confirmButtonText: options.confirmText || '확인',
cancelButtonText: options.cancelText || '취소',
showCancelButton: true,
reverseButtons: true,
};
SwalTailwind.fire({
...defaultOptions,
html: message,
}).then((result) => {
if (result.isConfirmed && typeof onConfirm === 'function') {
onConfirm();
}
});
}
/**
* 삭제 확인 모달 (위험 스타일)
* @param {string} itemName - 삭제할 항목명
* @param {Function} onConfirm - 확인 시 콜백
*/
function showDeleteConfirm(itemName, onConfirm) {
SwalTailwind.fire({
title: '삭제 확인',
html: `<span class="text-red-600 font-medium">"${itemName}"</span>을(를) 삭제하시겠습니까?`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: '삭제',
cancelButtonText: '취소',
reverseButtons: true,
customClass: {
popup: 'rounded-xl shadow-2xl border-0',
title: 'text-gray-900 font-semibold',
htmlContainer: 'text-gray-600',
confirmButton: 'bg-red-600 hover:bg-red-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-red-300',
cancelButton: 'bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-gray-100',
actions: 'gap-3',
},
buttonsStyling: false,
}).then((result) => {
if (result.isConfirmed && typeof onConfirm === 'function') {
onConfirm();
}
});
}
/**
* 영구 삭제 확인 모달 (매우 위험)
* @param {string} itemName - 삭제할 항목명
* @param {Function} onConfirm - 확인 시 콜백
*/
function showPermanentDeleteConfirm(itemName, onConfirm) {
SwalTailwind.fire({
title: '⚠️ 영구 삭제',
html: `<span class="text-red-600 font-bold">"${itemName}"</span>을(를) 영구 삭제하시겠습니까?<br><br><span class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다!</span>`,
icon: 'error',
showCancelButton: true,
confirmButtonText: '영구 삭제',
cancelButtonText: '취소',
reverseButtons: true,
customClass: {
popup: 'rounded-xl shadow-2xl border-0',
title: 'text-red-600 font-bold',
htmlContainer: 'text-gray-600',
confirmButton: 'bg-red-600 hover:bg-red-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-red-300',
cancelButton: 'bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-gray-100',
actions: 'gap-3',
},
buttonsStyling: false,
}).then((result) => {
if (result.isConfirmed && typeof onConfirm === 'function') {
onConfirm();
}
});
}
/**
* 성공 알림 모달
* @param {string} message - 메시지
* @param {Function} onClose - 닫기 후 콜백 (선택)
*/
function showSuccess(message, onClose = null) {
SwalTailwind.fire({
title: '완료',
text: message,
icon: 'success',
confirmButtonText: '확인',
}).then(() => {
if (typeof onClose === 'function') {
onClose();
}
});
}
/**
* 에러 알림 모달
* @param {string} message - 에러 메시지
*/
function showError(message) {
SwalTailwind.fire({
title: '오류',
text: message,
icon: 'error',
confirmButtonText: '확인',
});
}
</script>
<!-- 사이드바 토글 스크립트 -->
<script>
// 사이드바 상태 관리
const SIDEBAR_STATE_KEY = 'sidebar-collapsed';
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const html = document.documentElement;
const isCollapsed = sidebar.classList.contains('sidebar-collapsed') || html.classList.contains('sidebar-is-collapsed');
if (isCollapsed) {
// 펼치기
sidebar.classList.remove('sidebar-collapsed');
html.classList.remove('sidebar-is-collapsed');
localStorage.setItem(SIDEBAR_STATE_KEY, 'false');
} else {
// 접기
sidebar.classList.add('sidebar-collapsed');
html.classList.add('sidebar-is-collapsed');
localStorage.setItem(SIDEBAR_STATE_KEY, 'true');
}
}
// 페이지 로드 시 저장된 사이드바 상태 복원 (클래스 동기화)
document.addEventListener('DOMContentLoaded', function() {
const sidebar = document.getElementById('sidebar');
const html = document.documentElement;
const savedState = localStorage.getItem(SIDEBAR_STATE_KEY);
if (savedState === 'true') {
sidebar.classList.add('sidebar-collapsed');
html.classList.add('sidebar-is-collapsed');
} else {
sidebar.classList.remove('sidebar-collapsed');
html.classList.remove('sidebar-is-collapsed');
}
});
</script>
@stack('scripts')
</body>
</html>