This commit is contained in:
2025-04-17 23:32:39 +09:00
commit 5b719b6766
105 changed files with 25653 additions and 0 deletions
+533
View File
@@ -0,0 +1,533 @@
<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>&copy; {{ 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 = ref(!!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) {
localStorage.setItem('userClubRole', clubs.value[0].memberType || '');
}
} 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 router = useRouter();
const onClubChange = async () => {
try {
// 선택된 클럽의 memberType 찾기
const selectedClub = clubs.value.find(club => club.id === selectedClubId.value);
if (selectedClub) {
localStorage.setItem('userClubRole', selectedClub.memberType || '');
localStorage.setItem('clubId', selectedClub.id);
}
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);
}
}
};
// 컴포넌트 마운트 시 데이터 로드
onMounted(async () => {
await loadUserData();
if (isLoggedIn.value) {
await fetchClubs();
await fetchClubInfo();
loadMenuItems();
}
// 화면 크기 변경 감지
window.addEventListener('resize', handleResize);
// 초기 expandedGroups 설정
initializeExpandedGroups();
});
// 컴포넌트 언마운트 시 이벤트 리스너 제거
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});
</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>