- 품목관리 3-Panel 레이아웃 (좌:목록, 중:BOM/수식산출, 우:상세) - FormulaApiService로 API 견적수식 엔진 연동 - FG 품목 선택 시 기본값(W:1000, H:1000, QTY:1) 자동 산출 - 수식 산출 결과 트리 렌더링 (그룹별/소계/합계) - 중앙 패널 클릭 시 우측 상세만 변경 (skipCenterUpdate) - API 인증 버튼 전역 헤더로 이동 (모든 페이지에서 사용 가능) - FormulaApiService에 Bearer 토큰 지원 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
578 lines
23 KiB
PHP
578 lines
23 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') }}',
|
|
debug: {{ config('app.debug') ? 'true' : 'false' }},
|
|
};
|
|
|
|
// 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>
|
|
<!-- 페이지 로딩 오버레이 스타일 -->
|
|
<style id="page-loader-styles">
|
|
.page-loader-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 99999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
}
|
|
.page-loader-content {
|
|
text-align: center;
|
|
}
|
|
.page-loader-bar {
|
|
width: 200px;
|
|
height: 4px;
|
|
background: #e5e7eb;
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
.page-loader-progress {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
|
border-radius: 2px;
|
|
animation: loader-progress 1.5s ease-in-out infinite;
|
|
}
|
|
@keyframes loader-progress {
|
|
0% { width: 0%; margin-left: 0; }
|
|
50% { width: 60%; margin-left: 20%; }
|
|
100% { width: 0%; margin-left: 100%; }
|
|
}
|
|
.page-loader-text {
|
|
margin-top: 12px;
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
}
|
|
/* 블러 해제 애니메이션 */
|
|
.page-loader-overlay.fade-out {
|
|
animation: loader-fade-out 0.3s ease-out forwards;
|
|
}
|
|
@keyframes loader-fade-out {
|
|
to {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
}
|
|
/* select 요소 텍스트-화살표 겹침 방지 */
|
|
select {
|
|
padding-right: 2rem !important;
|
|
}
|
|
</style>
|
|
<!-- 사이드바 + 메뉴 상태 즉시 적용 -->
|
|
<script>
|
|
(function() {
|
|
// 사이드바 접힘 상태
|
|
if (localStorage.getItem('sidebar-collapsed') === 'true') {
|
|
document.documentElement.classList.add('sidebar-is-collapsed');
|
|
}
|
|
// 메뉴 그룹: hidden 상태인 것만 CSS로 숨김
|
|
var style = document.createElement('style');
|
|
style.id = 'menu-group-preload-styles';
|
|
var css = '';
|
|
var iconCss = '';
|
|
for (var i = 0; i < localStorage.length; i++) {
|
|
var key = localStorage.key(i);
|
|
if (key && key.startsWith('menu-group-menu-group-')) {
|
|
var groupId = key.replace('menu-group-', '');
|
|
var state = localStorage.getItem(key);
|
|
if (state === 'hidden') {
|
|
css += '#' + groupId + '{display:none!important}';
|
|
iconCss += '#' + groupId + '-icon{transform:rotate(0deg)!important}';
|
|
}
|
|
}
|
|
}
|
|
if (css || iconCss) {
|
|
style.textContent = css + iconCss;
|
|
document.head.appendChild(style);
|
|
}
|
|
// 스크롤 위치 미리 저장
|
|
window._savedSidebarScroll = localStorage.getItem('sidebar-scroll-top');
|
|
window._savedSidebarScrollBottom = localStorage.getItem('sidebar-scroll-bottom');
|
|
})();
|
|
</script>
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
<!-- Alpine.js Plugins (Collapse) - 반드시 Alpine.js 전에 로드 -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
|
<!-- Alpine.js -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
<!-- HTMX 전역 설정 (HTMX 로드 직후 등록하여 load 트리거보다 먼저 실행) -->
|
|
<script>
|
|
// HTMX 에러 핸들러 (401 세션 만료 / 419 CSRF 만료)
|
|
document.addEventListener('htmx:responseError', function(event) {
|
|
const status = event.detail.xhr.status;
|
|
|
|
// 419 CSRF 토큰 만료 → 페이지 새로고침으로 토큰 갱신
|
|
if (status === 419) {
|
|
console.log('[Session] CSRF 토큰 만료, 페이지 새로고침');
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
|
|
if (status !== 401) return;
|
|
|
|
// 중복 처리 방지
|
|
if (window._sessionRefreshing) return;
|
|
window._sessionRefreshing = true;
|
|
|
|
// 원본 요청 정보 저장
|
|
const originalPath = event.detail.pathInfo?.requestPath || window.location.pathname;
|
|
|
|
// 세션 갱신 시도 (Remember Token으로 재인증)
|
|
fetch('/auth/refresh-session', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
|
|
'Accept': 'application/json',
|
|
},
|
|
credentials: 'same-origin',
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
window._sessionRefreshing = false;
|
|
|
|
if (data.success) {
|
|
// 세션 갱신 성공 - 페이지 전체 새로고침 (CSRF 토큰도 갱신)
|
|
console.log('[Session] 세션 갱신 성공, 페이지 새로고침');
|
|
window.location.reload();
|
|
} else {
|
|
// 세션 갱신 실패 - 로그인 페이지로 이동
|
|
redirectToLogin(data.message || '세션이 만료되었습니다. 다시 로그인해주세요.');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
window._sessionRefreshing = false;
|
|
console.error('[Session] 세션 갱신 실패:', error);
|
|
redirectToLogin('세션이 만료되었습니다. 다시 로그인해주세요.');
|
|
});
|
|
});
|
|
|
|
// 로그인 페이지로 리다이렉트 (중복 방지 + 명확한 팝업)
|
|
function redirectToLogin(message) {
|
|
if (window._sessionExpiredRedirecting) return;
|
|
window._sessionExpiredRedirecting = true;
|
|
|
|
if (typeof Swal !== 'undefined') {
|
|
Swal.fire({
|
|
icon: 'warning',
|
|
title: '세션 만료',
|
|
text: message,
|
|
confirmButtonText: '로그인 페이지로 이동',
|
|
allowOutsideClick: false,
|
|
allowEscapeKey: false,
|
|
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',
|
|
},
|
|
buttonsStyling: false,
|
|
}).then(() => {
|
|
window.location.href = '/login';
|
|
});
|
|
} else {
|
|
alert(message);
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('htmx:configRequest', (event) => {
|
|
// CSRF 토큰 설정
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
|
if (csrfToken) {
|
|
event.detail.headers['X-CSRF-TOKEN'] = csrfToken.getAttribute('content');
|
|
}
|
|
|
|
// 페이지네이션 per_page 값 적용 (쿠키에서 읽기)
|
|
if (event.detail.parameters && 'per_page' in event.detail.parameters) {
|
|
const getCookieValue = (name) => {
|
|
const nameEQ = name + "=";
|
|
const ca = document.cookie.split(';');
|
|
for (let i = 0; i < ca.length; i++) {
|
|
let c = ca[i].trim();
|
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length);
|
|
}
|
|
return null;
|
|
};
|
|
const savedPerPage = getCookieValue('pagination_per_page') || '10';
|
|
event.detail.parameters['per_page'] = savedPerPage;
|
|
|
|
// perPageInput hidden input도 동기화
|
|
const perPageInput = document.getElementById('perPageInput');
|
|
if (perPageInput) {
|
|
perPageInput.value = savedPerPage;
|
|
}
|
|
}
|
|
});
|
|
|
|
// HTMX 페이지 전환 후 페이지별 스크립트 초기화 (hx-boost 네비게이션용)
|
|
document.addEventListener('htmx:afterSettle', function(event) {
|
|
// main-content 교체 시에만 처리
|
|
if (event.detail.target && event.detail.target.id === 'main-content') {
|
|
initPageScripts();
|
|
}
|
|
});
|
|
|
|
// 페이지별 스크립트 초기화 함수
|
|
function initPageScripts() {
|
|
const path = window.location.pathname;
|
|
|
|
// /menus 페이지: SortableJS 및 필터 폼 초기화
|
|
if (path === '/menus' || path.startsWith('/menus/')) {
|
|
// SortableJS 초기화 (테이블 로드 후)
|
|
if (typeof initMenuSortable === 'function') {
|
|
setTimeout(() => initMenuSortable(), 100);
|
|
}
|
|
// 필터 폼 이벤트 리스너 재연결
|
|
const filterForm = document.getElementById('filterForm');
|
|
if (filterForm && !filterForm._htmxInitialized) {
|
|
filterForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
htmx.trigger('#menu-table', 'filterSubmit');
|
|
});
|
|
filterForm._htmxInitialized = true;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<!-- SweetAlert2 -->
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
@stack('styles')
|
|
</head>
|
|
<body class="bg-gray-100 no-transition">
|
|
<!-- 페이지 로딩 오버레이 -->
|
|
<div id="page-loader" class="page-loader-overlay">
|
|
<div class="page-loader-content">
|
|
<div class="page-loader-bar">
|
|
<div class="page-loader-progress"></div>
|
|
</div>
|
|
<div class="page-loader-text">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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 id="main-content" 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>
|
|
|
|
<!-- API 인증 모달 (전역) -->
|
|
@include('dev-tools.partials.auth-modal')
|
|
|
|
<!-- 전역 컨텍스트 메뉴 -->
|
|
@include('components.context-menu')
|
|
|
|
<!-- 테넌트 정보 모달 -->
|
|
@include('components.tenant-modal')
|
|
|
|
<!-- 사용자 정보 모달 -->
|
|
@include('components.user-modal')
|
|
|
|
<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>
|
|
<!-- SortableJS (메뉴 관리 등 드래그 앤 드롭용) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
|
<script src="{{ asset('js/menu-sortable.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>
|
|
|
|
<!-- API 인증 스크립트 (전역) -->
|
|
@include('dev-tools.partials.auth-scripts')
|
|
|
|
<!-- 헤더 API 인증 상태 동기화 -->
|
|
<script>
|
|
(function() {
|
|
function updateHeaderAuthBtn() {
|
|
const btn = document.getElementById('header-api-auth-btn');
|
|
if (!btn) return;
|
|
const dot = btn.querySelector('.dev-tools-auth-dot');
|
|
const label = btn.querySelector('.dev-tools-auth-status');
|
|
const isAuth = window.DevToolsAuth && DevToolsAuth.isAuthenticated();
|
|
|
|
if (dot) {
|
|
dot.classList.toggle('bg-green-500', isAuth);
|
|
dot.classList.toggle('bg-gray-300', !isAuth);
|
|
}
|
|
if (label) {
|
|
label.textContent = isAuth ? 'API 인증됨' : 'API 인증';
|
|
label.classList.toggle('text-green-600', isAuth);
|
|
label.classList.toggle('text-gray-500', !isAuth);
|
|
}
|
|
btn.classList.toggle('border-green-300', isAuth);
|
|
btn.classList.toggle('border-gray-300', !isAuth);
|
|
}
|
|
|
|
// DevToolsAuth 콜백 등록
|
|
if (window.DevToolsAuth) {
|
|
DevToolsAuth.onAuthChange(updateHeaderAuthBtn);
|
|
}
|
|
|
|
// 초기 상태 동기화
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', updateHeaderAuthBtn);
|
|
} else {
|
|
updateHeaderAuthBtn();
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
@stack('scripts')
|
|
</body>
|
|
</html>
|