569 lines
14 KiB
Vue
569 lines
14 KiB
Vue
<template>
|
|
<div class="app">
|
|
<AppHeader />
|
|
|
|
<div class="app-content">
|
|
<!-- 사이드바 오버레이 추가 -->
|
|
<div v-if="isSidebarVisible && isMobileView" class="sidebar-overlay" @click="closeSidebar"></div>
|
|
|
|
<!-- 사이드바에 모바일 클래스 추가 -->
|
|
<aside v-if="isLoggedIn" class="sidebar" :class="{ 'mobile-visible': isSidebarVisible, 'mobile-hidden': !isSidebarVisible && isMobileView }">
|
|
<div class="sidebar-content">
|
|
<!-- 클럽 정보 -->
|
|
<div class="club-info" v-if="selectedClub">
|
|
<div class="club-header">
|
|
<h3>{{ selectedClub.name }}</h3>
|
|
<p class="club-location"><i class="pi pi-map-marker"></i> {{ selectedClub.location }}</p>
|
|
</div>
|
|
<div class="club-stats">
|
|
<div class="stat-item">
|
|
<i class="pi pi-users"></i>
|
|
<span>{{ memberCount }} 명</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<i class="pi pi-user"></i>
|
|
<span>모임장: {{ ownerName }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 클럽 선택 기능 (관리자 또는 클럽이 2개 이상) -->
|
|
<div v-if="isAdmin || clubs.length > 1" class="club-selector">
|
|
<label for="club-select">클럽 선택:</label>
|
|
<select id="club-select" v-model="selectedClubId" @change="onClubChange">
|
|
<option v-for="club in clubs" :key="club.id" :value="club.id">{{ club.name }}({{ club.memberType }})</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 커스텀 사이드바 메뉴 -->
|
|
<div class="custom-sidebar-menu">
|
|
<template v-for="(menuGroup, groupIndex) in menuItems" :key="groupIndex">
|
|
<div class="menu-group">
|
|
<div class="menu-group-header" @click="toggleMenuGroup(groupIndex)">
|
|
<i :class="menuGroup.icon"></i>
|
|
<span>{{ menuGroup.label }}</span>
|
|
<i class="pi" :class="expandedGroups[groupIndex] ? 'pi-chevron-down' : 'pi-chevron-right'"></i>
|
|
</div>
|
|
|
|
<div class="menu-items" v-if="expandedGroups[groupIndex]" v-show="menuGroup.items && menuGroup.items.length">
|
|
<router-link
|
|
v-for="(item, itemIndex) in menuGroup.items"
|
|
:key="itemIndex"
|
|
:to="item.to"
|
|
class="menu-item"
|
|
exact
|
|
active-class="router-link-active"
|
|
@click="closeSidebarOnMobile"
|
|
>
|
|
<i :class="item.icon"></i>
|
|
<span>{{ item.label }}</span>
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 그룹이 없는 단일 메뉴 아이템 -->
|
|
<template v-for="(menuItem, itemIndex) in singleMenuItems" :key="'single-'+itemIndex">
|
|
<router-link
|
|
:to="menuItem.to"
|
|
class="menu-item single-menu-item"
|
|
exact
|
|
active-class="router-link-active"
|
|
@click="closeSidebarOnMobile"
|
|
>
|
|
<i :class="menuItem.icon"></i>
|
|
<span>{{ menuItem.label }}</span>
|
|
</router-link>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main-content" :class="{ 'full-width': !isLoggedIn }">
|
|
<router-view />
|
|
</main>
|
|
</div>
|
|
|
|
<footer>
|
|
<p>© {{ new Date().getFullYear() }} 볼링 클럽 매니저. All rights reserved.</p>
|
|
</footer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onBeforeUnmount, provide } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import AppHeader from './components/AppHeader.vue';
|
|
import apiClient from './services/api';
|
|
import authService from './services/authService';
|
|
|
|
// 상태 변수
|
|
const user = ref(null);
|
|
provide('user', user);
|
|
const menuItems = ref([]);
|
|
const singleMenuItems = ref([]);
|
|
const expandedGroups = ref({});
|
|
const isMobileView = ref(window.innerWidth < 768);
|
|
const isSidebarVisible = ref(!isMobileView.value);
|
|
const isLoggedIn = computed(() => !!user.value && !!localStorage.getItem('token'));
|
|
const clubs = ref([]); // 클럽 목록
|
|
const selectedClubId = ref(localStorage.getItem('clubId') || null); // 선택된 클럽 ID
|
|
const selectedClub = ref(null);
|
|
const memberCount = ref(0);
|
|
const ownerName = ref('');
|
|
|
|
const userRole = computed(() => user.value?.role || '');
|
|
const isAdmin = computed(() => userRole.value === 'admin' || userRole.value === 'superadmin');
|
|
|
|
// 화면 크기 변경 감지
|
|
const handleResize = () => {
|
|
isMobileView.value = window.innerWidth < 768;
|
|
if (!isMobileView.value) {
|
|
isSidebarVisible.value = true;
|
|
}
|
|
};
|
|
|
|
// 사이드바 토글
|
|
const toggleSidebar = () => {
|
|
isSidebarVisible.value = !isSidebarVisible.value;
|
|
};
|
|
|
|
// 사이드바 닫기
|
|
const closeSidebar = () => {
|
|
if (isMobileView.value) {
|
|
isSidebarVisible.value = false;
|
|
}
|
|
};
|
|
|
|
// 모바일에서 메뉴 아이템 클릭 시 사이드바 닫기
|
|
const closeSidebarOnMobile = () => {
|
|
if (isMobileView.value) {
|
|
closeSidebar();
|
|
}
|
|
};
|
|
|
|
// 메뉴 그룹 토글
|
|
const toggleMenuGroup = (groupIndex) => {
|
|
expandedGroups.value[groupIndex] = !expandedGroups.value[groupIndex];
|
|
};
|
|
|
|
// 모든 메뉴 그룹 초기화 (모두 펼치기)
|
|
const initializeExpandedGroups = () => {
|
|
menuItems.value.forEach((_, index) => {
|
|
expandedGroups.value[index] = true;
|
|
});
|
|
};
|
|
|
|
// 클럽 목록 가져오기
|
|
const fetchClubs = async () => {
|
|
try {
|
|
const userId = user.value?.id;
|
|
const userRole = localStorage.getItem('userRole');
|
|
|
|
const response = await apiClient.post('/api/club/user', {
|
|
userId,
|
|
role: userRole
|
|
});
|
|
|
|
clubs.value = response.data.map(club => ({
|
|
...club,
|
|
memberType: club.memberType || ''
|
|
}));
|
|
|
|
// 첫 번째 클럽의 memberType을 localStorage에 저장
|
|
if (clubs.value.length > 0 && !selectedClubId.value) {
|
|
selectedClubId.value = clubs.value[0].id;
|
|
await setSelectedClub(clubs.value[0]);
|
|
}
|
|
} catch (error) {
|
|
console.error('클럽 목록 로드 실패:', error);
|
|
}
|
|
};
|
|
|
|
// 클럽 정보 가져오기
|
|
const fetchClubInfo = async () => {
|
|
try {
|
|
const response = await apiClient.post(`/api/club`, { clubId: selectedClubId.value ?? clubs.value[0].id });
|
|
|
|
if (response.data) {
|
|
selectedClub.value = {
|
|
id: response.data.id,
|
|
name: response.data.name,
|
|
location: response.data.location,
|
|
};
|
|
memberCount.value = response.data.memberCount;
|
|
ownerName.value = response.data.ownerName;
|
|
}
|
|
} catch (error) {
|
|
console.error('클럽 정보 가져오기 오류:', error);
|
|
}
|
|
};
|
|
|
|
// 클럽 선택
|
|
const setSelectedClub = async (club) => {
|
|
// 1. localStorage에 저장
|
|
localStorage.setItem('clubId', club.id);
|
|
localStorage.setItem('userClubRole', club.memberType || '');
|
|
|
|
// 2. 세션에도 저장 (백엔드에 clubId 전달)
|
|
try {
|
|
await apiClient.post('/api/club/select-club', { clubId: club.id });
|
|
} catch (error) {
|
|
console.error('클럽 ID 세션 저장 실패:', error);
|
|
}
|
|
|
|
// 3. 프론트엔드 상태도 변경
|
|
selectedClubId.value = club.id;
|
|
};
|
|
|
|
// 클럽 변경 시
|
|
const router = useRouter();
|
|
const onClubChange = async () => {
|
|
try {
|
|
const selectedClub = clubs.value.find(club => club.id === selectedClubId.value);
|
|
if (selectedClub) {
|
|
await setSelectedClub(selectedClub);
|
|
}
|
|
|
|
await fetchClubInfo();
|
|
loadMenuItems();
|
|
|
|
// 라우터의 네비게이션을 강제로 새로고침
|
|
router.go(0);
|
|
} catch (error) {
|
|
console.error('클럽 변경 실패:', error);
|
|
}
|
|
};
|
|
|
|
// 메뉴 아이템 로드
|
|
const loadMenuItems = async () => {
|
|
try {
|
|
const role = localStorage.getItem('userRole');
|
|
const clubId = selectedClubId.value;
|
|
|
|
if (!clubId) {
|
|
throw new Error('클럽 ID가 없습니다.');
|
|
}
|
|
|
|
try {
|
|
const response = await apiClient.post(`/api/menus`, {
|
|
role: role,
|
|
clubId: clubId
|
|
});
|
|
|
|
if (response.data && response.data.success && response.data.data && response.data.data.length > 0) {
|
|
const groupMenus = [];
|
|
const singleMenus = [];
|
|
|
|
// 서버에서 받은 메뉴 처리 (관리자 메뉴 제외)
|
|
response.data.data
|
|
.forEach(menuGroup => {
|
|
if (menuGroup.children && menuGroup.children.length > 0) {
|
|
const processedItems = menuGroup.children.map(item => ({
|
|
label: item.title,
|
|
icon: item.icon,
|
|
to: item.to
|
|
}));
|
|
|
|
groupMenus.push({
|
|
label: menuGroup.title,
|
|
icon: menuGroup.icon,
|
|
items: processedItems
|
|
});
|
|
} else if (menuGroup.to) {
|
|
singleMenus.push({
|
|
label: menuGroup.title,
|
|
icon: menuGroup.icon,
|
|
to: menuGroup.to
|
|
});
|
|
}
|
|
});
|
|
|
|
menuItems.value = groupMenus;
|
|
singleMenuItems.value = singleMenus;
|
|
} else {
|
|
menuItems.value = [];
|
|
singleMenuItems.value = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('메뉴 아이템 로드 오류:', error);
|
|
menuItems.value = [];
|
|
singleMenuItems.value = [];
|
|
}
|
|
|
|
initializeExpandedGroups();
|
|
} catch (error) {
|
|
console.error('메뉴 아이템 로드 오류:', error);
|
|
menuItems.value = [];
|
|
singleMenuItems.value = [];
|
|
}
|
|
};
|
|
|
|
// 사용자 정보 로드
|
|
const loadUserData = async () => {
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
try {
|
|
const response = await authService.getCurrentUser();
|
|
user.value = response.data;
|
|
} catch (error) {
|
|
console.error('사용자 정보 로드 실패:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 로그인 성공 후 상태 갱신 핸들러
|
|
const handleUserLoggedIn = async () => {
|
|
await loadUserData();
|
|
if (isLoggedIn.value) {
|
|
await fetchClubs();
|
|
await fetchClubInfo();
|
|
loadMenuItems();
|
|
}
|
|
};
|
|
|
|
// 컴포넌트 마운트 시 데이터 로드
|
|
onMounted(async () => {
|
|
await loadUserData();
|
|
const clubId = localStorage.getItem('clubId');
|
|
if (clubId) {
|
|
// 서버 세션에 clubId를 동기화
|
|
apiClient.post('/api/club/select-club', { clubId });
|
|
}
|
|
if (isLoggedIn.value) {
|
|
await fetchClubs();
|
|
await fetchClubInfo();
|
|
loadMenuItems();
|
|
}
|
|
|
|
// 화면 크기 변경 감지
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
// 로그인 성공 이벤트 감지
|
|
window.addEventListener('user-logged-in', handleUserLoggedIn);
|
|
|
|
// 초기 expandedGroups 설정
|
|
initializeExpandedGroups();
|
|
});
|
|
|
|
// 컴포넌트 언마운트 시 이벤트 리스너 제거
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', handleResize);
|
|
window.removeEventListener('user-logged-in', handleUserLoggedIn);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* App.vue 스타일 */
|
|
.app {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.app-content {
|
|
display: flex;
|
|
flex: 1;
|
|
}
|
|
|
|
/* 사이드바 스타일 */
|
|
.sidebar {
|
|
width: 250px;
|
|
background-color: #f8f9fa;
|
|
border-right: 1px solid #dee2e6;
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.sidebar.mobile-visible {
|
|
transform: translateX(0);
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.sidebar.mobile-hidden {
|
|
transform: translateX(-100%);
|
|
}
|
|
|
|
.sidebar-content {
|
|
padding: 20px;
|
|
}
|
|
|
|
.club-info {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.club-header {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.club-header h3 {
|
|
margin: 0;
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.club-location {
|
|
margin: 0;
|
|
font-size: 0.9em;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.club-stats {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.stat-item {
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.stat-item i {
|
|
margin-right: 5px;
|
|
font-size: 1em;
|
|
}
|
|
|
|
.stat-item span {
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
/* 클럽 선택 스타일 */
|
|
.club-selector {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.club-selector label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.club-selector select {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid #ced4da;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* 사이드바 메뉴 스타일 */
|
|
.custom-sidebar-menu {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.menu-group-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 10px 15px;
|
|
background-color: #e9ecef;
|
|
border-radius: 5px;
|
|
margin-bottom: 5px;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
|
|
.menu-group-header:hover {
|
|
background-color: #dee2e6;
|
|
}
|
|
|
|
.menu-group-header i {
|
|
margin-right: 10px;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.menu-group-header span {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.menu-items {
|
|
padding-left: 15px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px 15px;
|
|
border-radius: 5px;
|
|
text-decoration: none;
|
|
color: #495057;
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
|
|
.menu-item:hover {
|
|
background-color: #f1f3f5;
|
|
}
|
|
|
|
.menu-item i {
|
|
margin-right: 10px;
|
|
font-size: 1em;
|
|
}
|
|
|
|
.menu-item span {
|
|
font-size: 1em;
|
|
}
|
|
|
|
.router-link-active {
|
|
background-color: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
.router-link-active:hover {
|
|
background-color: #0069d9;
|
|
}
|
|
|
|
.single-menu-item {
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
/* 메인 컨텐츠 스타일 */
|
|
.main-content {
|
|
flex: 1;
|
|
padding: 20px;
|
|
transition: margin-left 0.3s ease;
|
|
}
|
|
|
|
.main-content.full-width {
|
|
margin-left: 0;
|
|
}
|
|
|
|
/* 푸터 스타일 */
|
|
footer {
|
|
background-color: #343a40;
|
|
color: white;
|
|
text-align: center;
|
|
padding: 10px;
|
|
}
|
|
|
|
/* 사이드바 오버레이 스타일 */
|
|
.sidebar-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 1;
|
|
}
|
|
|
|
/* 미디어 쿼리 (모바일 뷰) */
|
|
@media (max-width: 767px) {
|
|
.sidebar {
|
|
width: 200px;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 3;
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.main-content {
|
|
padding: 10px;
|
|
}
|
|
}
|
|
</style>
|