Files
sam-manage/public/js/tenant-modal.js
kent 7c7c04f8dc feat: 테넌트 모달 하단 버튼 플로팅 고정
- 닫기/수정 버튼을 모달 하단에 고정 (flex-shrink-0)
- 콘텐츠 스크롤 시에도 버튼 항상 표시
- 기존 modal-info 내부 버튼 제거

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 00:11:57 +09:00

252 lines
8.3 KiB
JavaScript

/**
* 테넌트 정보 모달
* 테넌트 상세 정보를 팝업으로 표시하고 탭으로 관련 정보 관리
*/
const TenantModal = {
modalElement: null,
currentTenantId: null,
currentTab: 'info',
init() {
this.modalElement = document.getElementById('tenant-modal');
if (!this.modalElement) return;
// ESC 키로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen()) {
this.close();
}
});
// 배경 클릭 시 모달 닫기
this.modalElement.addEventListener('click', (e) => {
if (e.target === this.modalElement) {
this.close();
}
});
},
isOpen() {
return this.modalElement && !this.modalElement.classList.contains('hidden');
},
async open(tenantId) {
if (!this.modalElement) {
console.error('Tenant modal element not found');
return;
}
this.currentTenantId = tenantId;
this.currentTab = 'subscription';
// 모달 표시
this.modalElement.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// 로딩 표시
this.showLoading();
// 테넌트 정보 로드
await this.loadTenantInfo();
},
close() {
if (this.modalElement) {
this.modalElement.classList.add('hidden');
document.body.style.overflow = '';
}
// 푸터 숨기기
const footer = document.getElementById('tenant-modal-footer');
if (footer) {
footer.classList.add('hidden');
}
this.currentTenantId = null;
},
showLoading() {
const content = document.getElementById('tenant-modal-content');
if (content) {
content.innerHTML = `
<div class="flex items-center justify-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
`;
}
},
async loadTenantInfo() {
try {
const response = await fetch(`/api/admin/tenants/${this.currentTenantId}/modal`, {
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json'
}
});
const data = await response.json();
if (data.html) {
const content = document.getElementById('tenant-modal-content');
if (content) {
content.innerHTML = data.html;
}
// 푸터 표시
const footer = document.getElementById('tenant-modal-footer');
if (footer) {
footer.classList.remove('hidden');
}
// 구독정보 탭 자동 로드
this.currentTab = ''; // switchTab에서 early return 방지
await this.switchTab('subscription');
}
} catch (error) {
console.error('Failed to load tenant info:', error);
this.showError('테넌트 정보를 불러오는데 실패했습니다.');
}
},
async switchTab(tab) {
if (this.currentTab === tab) return;
this.currentTab = tab;
// 탭 버튼 스타일 업데이트
document.querySelectorAll('#tenant-modal [data-tab-btn]').forEach(btn => {
if (btn.dataset.tabBtn === tab) {
btn.classList.add('border-blue-500', 'text-blue-600');
btn.classList.remove('border-transparent', 'text-gray-500');
} else {
btn.classList.remove('border-blue-500', 'text-blue-600');
btn.classList.add('border-transparent', 'text-gray-500');
}
});
// 탭 콘텐츠 로드
const tabContent = document.getElementById('tenant-modal-tab-content');
if (!tabContent) return;
// 로딩 표시
tabContent.innerHTML = `
<div class="flex items-center justify-center h-32">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
`;
try {
const response = await fetch(`/api/admin/tenants/${this.currentTenantId}/${tab}`, {
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json'
}
});
const data = await response.json();
if (data.html) {
tabContent.innerHTML = data.html;
}
} catch (error) {
console.error(`Failed to load ${tab} tab:`, error);
tabContent.innerHTML = `
<div class="text-center py-8 text-red-500">
데이터를 불러오는데 실패했습니다.
</div>
`;
}
},
showError(message) {
const content = document.getElementById('tenant-modal-content');
if (content) {
content.innerHTML = `
<div class="flex flex-col items-center justify-center h-64 text-red-500">
<svg class="w-12 h-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<p>${message}</p>
<button onclick="TenantModal.close()" class="mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">
닫기
</button>
</div>
`;
}
},
// 수정 페이지로 이동
goToEdit() {
if (this.currentTenantId) {
window.location.href = `/tenants/${this.currentTenantId}/edit`;
}
}
};
/**
* 모달 내 메뉴 자식 토글
* @param {number} menuId 부모 메뉴 ID
*/
window.toggleModalMenuChildren = function(menuId) {
const rows = document.querySelectorAll('.modal-menu-row');
const toggleBtn = document.querySelector(`.modal-menu-toggle[data-menu-id="${menuId}"]`);
const chevron = toggleBtn?.querySelector('.chevron-icon');
// 직접 자식들 찾기
const directChildren = [];
rows.forEach(row => {
if (row.dataset.parentId == menuId) {
directChildren.push(row);
}
});
if (directChildren.length === 0) return;
// 첫 번째 자식의 현재 상태 확인
const isHidden = directChildren[0].classList.contains('hidden');
if (isHidden) {
// 펼치기: 직접 자식만 표시
directChildren.forEach(child => {
child.classList.remove('hidden');
});
// 쉐브론 회전 초기화
if (chevron) {
chevron.classList.remove('-rotate-90');
}
} else {
// 접기: 모든 자손 숨기기 (재귀적으로)
window.hideModalMenuDescendants(menuId, rows);
// 쉐브론 회전
if (chevron) {
chevron.classList.add('-rotate-90');
}
}
};
/**
* 특정 메뉴의 모든 자손 숨기기 (재귀)
* @param {number} parentId 부모 메뉴 ID
* @param {NodeList} rows 모든 메뉴 행
*/
window.hideModalMenuDescendants = function(parentId, rows) {
rows.forEach(row => {
if (row.dataset.parentId == parentId) {
row.classList.add('hidden');
// 자식의 쉐브론도 접힌 상태로
const childToggle = row.querySelector('.modal-menu-toggle');
const childChevron = childToggle?.querySelector('.chevron-icon');
if (childChevron) {
childChevron.classList.add('-rotate-90');
}
// 손자들도 재귀적으로 숨기기
const childMenuId = row.dataset.menuId;
if (childMenuId) {
window.hideModalMenuDescendants(childMenuId, rows);
}
}
});
};
// DOM 로드 시 초기화
document.addEventListener('DOMContentLoaded', () => {
TenantModal.init();
});