public 기능 완료

This commit is contained in:
2025-05-07 21:02:50 +09:00
parent 283c8a7f4a
commit 1684fbf5d1
28 changed files with 2522 additions and 194 deletions
+108 -93
View File
@@ -1,100 +1,103 @@
<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>
<template v-if="isRouterReady">
<AppPublicLayout v-if="isPublicLayout">
<router-view />
</AppPublicLayout>
<div v-else 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="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 class="club-stats">
<div class="stat-item">
<i class="pi pi-users"></i>
<span>{{ memberCount }} </span>
</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 class="stat-item">
<i class="pi pi-user"></i>
<span>모임장: {{ ownerName }}</span>
</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>
<!-- 클럽 선택 기능 (관리자 또는 클럽이 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>
</div>
</aside>
<main class="main-content" :class="{ 'full-width': !isLoggedIn }">
<router-view />
</main>
</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>
<footer>
<p>&copy; {{ new Date().getFullYear() }} 볼링 클럽 매니저. All rights reserved.</p>
</footer>
</div>
</template>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, provide } from 'vue';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import AppHeader from './components/AppHeader.vue';
import AppPublicLayout from './layouts/AppPublicLayout.vue';
import apiClient from './services/api';
const route = useRoute();
const layoutComponent = computed(() => route.meta.layout === 'public' ? AppPublicLayout : 'div');
const isPublicLayout = computed(() => {
// 라우트 meta.layout이 public이거나, 경로가 /public/로 시작하면 public 레이아웃으로 간주
return route.meta.layout === 'public' || route.path.startsWith('/public/');
});
import authService from './services/authService';
// 상태 변수
@@ -111,7 +114,7 @@ const selectedClubId = ref(localStorage.getItem('clubId') || null); // 선택된
const selectedClub = ref(null);
const memberCount = ref(0);
const ownerName = ref('');
const isRouterReady = ref(false);
const userRole = computed(() => user.value?.role || '');
const isAdmin = computed(() => userRole.value === 'admin' || userRole.value === 'superadmin');
@@ -156,6 +159,7 @@ const initializeExpandedGroups = () => {
// 클럽 목록 가져오기
const fetchClubs = async () => {
if (isPublicLayout.value) return;
try {
const userId = user.value?.id;
const userRole = localStorage.getItem('userRole');
@@ -182,6 +186,7 @@ const fetchClubs = async () => {
// 클럽 정보 가져오기
const fetchClubInfo = async () => {
if (isPublicLayout.value) return;
try {
const response = await apiClient.post(`/api/club`, { clubId: selectedClubId.value ?? clubs.value[0].id });
@@ -201,6 +206,7 @@ const fetchClubInfo = async () => {
// 클럽 선택
const setSelectedClub = async (club) => {
if (isPublicLayout.value) return;
// 1. localStorage에 저장
localStorage.setItem('clubId', club.id);
localStorage.setItem('userClubRole', club.memberType || '');
@@ -237,6 +243,7 @@ const onClubChange = async () => {
// 메뉴 아이템 로드
const loadMenuItems = async () => {
if (isPublicLayout.value) return;
try {
const role = localStorage.getItem('userRole');
const clubId = selectedClubId.value;
@@ -324,10 +331,19 @@ const handleUserLoggedIn = async () => {
// 컴포넌트 마운트 시 데이터 로드
onMounted(async () => {
await router.isReady?.()
isRouterReady.value = true
// public 레이아웃이면 내부 데이터 로딩/초기화 완전 스킵
if (isPublicLayout.value) {
window.addEventListener('resize', handleResize);
window.addEventListener('user-logged-in', handleUserLoggedIn);
initializeExpandedGroups();
return;
}
// 내부(클럽) 페이지만 내부 데이터 로딩
await loadUserData();
const clubId = localStorage.getItem('clubId');
if (clubId) {
// 서버 세션에 clubId를 동기화
apiClient.post('/api/club/select-club', { clubId });
}
if (isLoggedIn.value) {
@@ -335,14 +351,8 @@ onMounted(async () => {
await fetchClubInfo();
loadMenuItems();
}
// 화면 크기 변경 감지
window.addEventListener('resize', handleResize);
// 로그인 성공 이벤트 감지
window.addEventListener('user-logged-in', handleUserLoggedIn);
// 초기 expandedGroups 설정
initializeExpandedGroups();
});
@@ -352,7 +362,12 @@ onBeforeUnmount(() => {
window.removeEventListener('user-logged-in', handleUserLoggedIn);
});
</script>
<style>
body {
margin: 0;
padding: 0;
}
</style>
<style scoped>
/* App.vue 스타일 */
.app {