Files
sam-manage/resources/views/layouts/app.blade.php
hskwon 767db6f513 feat: 품목 필드 관리 및 UI 개선
- ItemFieldController API 수정
- ItemFieldSeedingService 로직 개선
- Flow Tester 상세 화면 개선
- 레이아웃 및 프로젝트 상세 화면 수정
- 테이블 정렬 JS 추가
2025-12-12 08:51:54 +09:00

220 lines
8.4 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'])
<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">
<!-- Sidebar -->
@include('partials.sidebar')
<!-- Main Content Area -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 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>
<!-- 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>
@stack('scripts')
</body>
</html>