클럽 설정 기능

This commit is contained in:
2025-05-14 05:10:41 +09:00
parent 8e9b941a28
commit a2b5ce88a6
8 changed files with 731 additions and 40 deletions
+606
View File
@@ -0,0 +1,606 @@
<template>
<div class="club-settings-container">
<div class="page-header">
<h1>클럽 설정</h1>
<p>클럽 정보 설정을 관리합니다</p>
</div>
<div class="settings-content">
<!-- 기본 클럽 정보 섹션 -->
<div class="settings-section">
<h2>기본 정보</h2>
<div class="settings-form">
<div class="form-group">
<label for="clubName">클럽명</label>
<input type="text" id="clubName" v-model="clubInfo.name" class="form-control" />
</div>
<div class="form-group">
<label for="clubDescription">클럽 설명</label>
<textarea id="clubDescription" v-model="clubInfo.description" class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
<label for="clubLocation">위치</label>
<input type="text" id="clubLocation" v-model="clubInfo.location" class="form-control" />
</div>
<div class="form-group">
<label for="femaleHandicap">여성 기본핸디 설정</label>
<input type="number" id="femaleHandicap" v-model="clubInfo.femaleHandicap" class="form-control" min="0" max="100" />
<small class="form-text text-muted">여성 회원을 멤버(게스트) 등록할 기본 핸디캡 값입니다.</small>
</div>
<div class="form-actions">
<button @click="saveClubInfo" class="btn btn-primary">저장</button>
</div>
</div>
</div>
<!-- 구독 정보 섹션 -->
<div class="settings-section">
<h2>구독 정보</h2>
<div v-if="subscriptionInfo" class="subscription-info">
<div class="subscription-card">
<div class="subscription-header">
<h3>{{ subscriptionInfo.planName }}</h3>
<span class="subscription-status" :class="subscriptionStatusClass">{{ subscriptionStatusText }}</span>
</div>
<div class="subscription-details">
<div class="detail-item">
<span class="detail-label">시작일:</span>
<span class="detail-value">{{ formatDate(subscriptionInfo.startDate) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">만료일:</span>
<span class="detail-value">{{ formatDate(subscriptionInfo.endDate) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">회원 :</span>
<span class="detail-value">{{ clubInfo.memberCount }}/{{ subscriptionInfo.maxMembers }}</span>
</div>
<div v-if="subscriptionInfo.planPrice" class="detail-item">
<span class="detail-label"> 구독료:</span>
<span class="detail-value">{{ subscriptionInfo.planPrice }}</span>
</div>
</div>
</div>
<div class="subscription-actions">
<router-link :to="{ name: 'ClubSubscription' }" class="btn btn-primary">구독 관리</router-link>
</div>
</div>
<div v-else class="no-subscription">
<p>현재 활성화된 구독이 없습니다.</p>
<router-link :to="{ name: 'ClubSubscription' }" class="btn btn-primary">구독 신청하기</router-link>
</div>
</div>
<!-- 기능 정보 섹션 -->
<div class="settings-section">
<h2>기능 정보</h2>
<div class="features-list">
<div v-for="feature in features" :key="feature.id" class="feature-item">
<div class="feature-icon">
<i :class="feature.icon"></i>
</div>
<div class="feature-details">
<h4>{{ feature.name }}</h4>
<p>{{ feature.description }}</p>
<div class="feature-info">
<span class="feature-status" :class="{ 'active': feature.active }">
{{ feature.active ? '활성화' : '비활성화' }}
</span>
<span v-if="feature.active && feature.expiryDate" class="feature-expiry">
만료일: {{ formatDate(feature.expiryDate) }}
</span>
<span v-if="feature.includedInPlan" class="feature-included">
플랜 포함
</span>
<span v-else-if="feature.price" class="feature-price">
{{ feature.price }}/
</span>
</div>
</div>
</div>
<div v-if="features.length === 0" class="no-features">
<p>가격이 있는 추가 기능이 없습니다.</p>
</div>
</div>
<div class="subscription-actions">
<router-link :to="{ name: 'ClubSubscription' }" class="btn btn-primary">기능 추가 구매</router-link>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { useClubStore } from '@/stores/clubStore';
import clubService from '@/services/clubService';
const clubStore = useClubStore();
const router = useRouter();
const toast = useToast();
// 클럽 정보
const clubInfo = ref({
id: null,
name: '',
description: '',
location: '',
femaleHandicap: 15 // 기본값 설정
});
// 구독 정보
const subscriptionInfo = ref(null);
// 기능 목록
const features = ref([]);
// 구독 상태 텍스트 계산
const subscriptionStatusText = computed(() => {
if (!subscriptionInfo.value) return '없음';
const now = new Date();
const endDate = new Date(subscriptionInfo.value.endDate);
if (endDate < now) {
return '만료됨';
}
return '활성';
});
// 구독 상태 클래스 계산
const subscriptionStatusClass = computed(() => {
if (!subscriptionInfo.value) return '';
const now = new Date();
const endDate = new Date(subscriptionInfo.value.endDate);
if (endDate < now) {
return 'expired';
}
return 'active';
});
// 날짜 포맷 함수
const formatDate = (dateString) => {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
// 클럽 정보 로드
const loadClubInfo = async () => {
try {
const clubId = localStorage.getItem('clubId');
if (!clubId) {
toast.add({ severity: 'error', summary: '오류', detail: '클럽 정보를 찾을 수 없습니다.', life: 3000 });
return;
}
const clubData = await clubStore.fetchClubById(clubId);
clubInfo.value = {
id: clubData.id,
name: clubData.name,
description: clubData.description || '',
location: clubData.location || '',
memberCount: clubData.memberCount || 0,
femaleHandicap: clubData.femaleHandicap || 15
};
// 구독 정보 로드
await loadSubscriptionInfo(clubId);
// 기능 정보 로드
await loadFeatures(clubId);
} catch (error) {
console.error('클럽 정보 로드 오류:', error);
toast.add({ severity: 'error', summary: '오류', detail: '클럽 정보를 불러오는 중 오류가 발생했습니다.', life: 3000 });
}
};
// 구독 정보 로드
const loadSubscriptionInfo = async (clubId) => {
try {
const subscription = await clubStore.fetchClubSubscription(clubId);
if (subscription && subscription.id) {
subscriptionInfo.value = {
id: subscription.id,
planId: subscription.planId,
planName: subscription.SubscriptionPlan.name,
planDescription: subscription.SubscriptionPlan.description,
startDate: subscription.startDate,
endDate: subscription.endDate,
status: subscription.status,
maxMembers: subscription.SubscriptionPlan.maxMembers,
planPrice: subscription.SubscriptionPlan.price,
price: subscription.price
};
}
} catch (error) {
console.error('구독 정보 로드 오류:', error);
// 구독 정보가 없는 경우 무시
}
};
// 기능 정보 로드
const loadFeatures = async (clubId) => {
try {
// 구독 정보에서 기능 정보 가져오기
const clubFeatures = clubStore.subscriptionFeatures || [];
// 플랜에 포함된 기능 확인
const planFeatures = [];
if (subscriptionInfo.value && subscriptionInfo.value.planId) {
// 현재 구독 플랜에 포함된 기능 ID 목록 가져오기
const subscription = await clubStore.fetchClubSubscription(clubId);
if (subscription && subscription.SubscriptionPlan && subscription.SubscriptionPlan.Features) {
subscription.SubscriptionPlan.Features.forEach(feature => {
planFeatures.push(feature.id);
});
}
}
// 가격이 있는 추가 기능만 표시 (메모리에 따른 요구사항)
features.value = clubFeatures
.filter(feature => feature.price > 0)
.map(feature => ({
id: feature.id,
name: feature.name,
description: feature.description,
price: feature.price,
icon: getFeatureIcon(feature.type || 'default'),
active: feature.status === 'active',
expiryDate: feature.expiryDate,
includedInPlan: planFeatures.includes(feature.id) // 플랜에 포함된 기능인지 여부
}));
} catch (error) {
console.error('기능 정보 로드 오류:', error);
features.value = [];
}
};
// 기능 아이콘 매핑
const getFeatureIcon = (featureType) => {
const iconMap = {
'team': 'pi pi-users',
'statistics': 'pi pi-chart-bar',
'calendar': 'pi pi-calendar',
'export': 'pi pi-file-export',
'notification': 'pi pi-bell'
};
return iconMap[featureType] || 'pi pi-star';
};
// 클럽 정보 저장
const saveClubInfo = async () => {
try {
const clubId = clubInfo.value.id;
if (!clubId) {
toast.add({ severity: 'error', summary: '오류', detail: '클럽 정보를 찾을 수 없습니다.', life: 3000 });
return;
}
// clubService를 사용하여 클럽 정보 업데이트
await clubService.updateClub(clubId, {
name: clubInfo.value.name,
description: clubInfo.value.description,
location: clubInfo.value.location,
femaleHandicap: clubInfo.value.femaleHandicap
});
// 성공 메시지 표시
toast.add({ severity: 'success', summary: '성공', detail: '클럽 정보가 저장되었습니다.', life: 3000 });
// 클럽 정보 다시 불러오기
await loadClubInfo();
} catch (error) {
console.error('클럽 정보 저장 오류:', error);
toast.add({ severity: 'error', summary: '오류', detail: '클럽 정보를 저장하는 중 오류가 발생했습니다.', life: 3000 });
}
};
// 컴포넌트 마운트 시 데이터 로드
onMounted(() => {
loadClubInfo();
});
</script>
<style scoped>
.club-settings-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 2rem;
margin-bottom: 10px;
}
.page-header p {
color: #6c757d;
}
.settings-section {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 30px;
}
.settings-section h2 {
font-size: 1.5rem;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
}
textarea.form-control {
resize: vertical;
}
.form-text {
display: block;
margin-top: 5px;
font-size: 0.875rem;
}
.form-actions {
margin-top: 30px;
}
.btn {
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
cursor: pointer;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
background-color: #0069d9;
border-color: #0062cc;
}
/* 구독 정보 스타일 */
.subscription-card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.subscription-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.subscription-header h3 {
margin: 0;
font-size: 1.25rem;
}
.subscription-status {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
.subscription-status.active {
background-color: #d4edda;
color: #155724;
}
.subscription-status.expired {
background-color: #f8d7da;
color: #721c24;
}
.subscription-details {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(20%, 1fr));
gap: 15px;
}
.detail-item {
display: flex;
flex-direction: column;
}
.detail-label {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 5px;
}
.detail-value {
font-weight: 500;
}
.subscription-actions {
margin-top: 20px;
}
.no-subscription {
text-align: center;
padding: 30px;
background-color: #f8f9fa;
border-radius: 8px;
}
.no-subscription p {
margin-bottom: 20px;
color: #6c757d;
}
/* 기능 정보 스타일 */
.features-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.feature-item {
display: flex;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.feature-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
background-color: #e9ecef;
border-radius: 50%;
margin-right: 15px;
}
.feature-icon i {
font-size: 1.5rem;
color: #007bff;
}
.feature-details {
flex: 1;
}
.feature-details h4 {
margin: 0 0 8px 0;
font-size: 1.1rem;
}
.feature-details p {
margin: 0 0 10px 0;
font-size: 0.9rem;
color: #6c757d;
}
.feature-info {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.feature-status {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
background-color: #f8d7da;
color: #721c24;
}
.feature-status.active {
background-color: #d4edda;
color: #155724;
}
.feature-expiry {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
background-color: #e2e3e5;
color: #383d41;
}
.feature-price {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
background-color: #cce5ff;
color: #004085;
}
.feature-included {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
background-color: #d1ecf1;
color: #0c5460;
}
.no-features {
text-align: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
color: #6c757d;
}
/* 반응형 스타일 */
@media (max-width: 768px) {
.features-list {
grid-template-columns: 1fr;
}
.subscription-details {
grid-template-columns: 1fr;
}
}
</style>
@@ -21,6 +21,8 @@
stripedRows
filterDisplay="menu"
v-model:filters="filters"
sort-field="startDate"
:sort-order="-1"
class="mt-4"
>
<Column field="eventType" header="유형" sortable style="width: 10%">
+61 -33
View File
@@ -22,6 +22,8 @@
<DataTable :value="filteredMembers" :paginator="true" :rows="10"
:rowsPerPageOptions="[5, 10, 20]"
sortField="name"
:sortOrder="1"
stripedRows responsiveLayout="scroll">
<Column field="name" header="이름" sortable style="width: 15%"></Column>
<Column field="gender" header="성별" sortable style="width: 10%"></Column>
@@ -62,29 +64,35 @@
:header="dialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
:modal="true" class="p-fluid">
<!-- 회원 정보 다이얼로그 내부 예시 -->
<div class="field mb-3">
<label for="gender" class="mb-1">성별</label>
<Select id="gender" v-model="member.gender" :options="genderOptions" optionLabel="name" placeholder="성별 선택" class="w-full" />
</div>
<div class="field mb-3">
<label for="memberType" class="mb-1">회원 유형</label>
<Select id="memberType" v-model="member.memberTypeObj" :options="memberTypes" optionLabel="name" placeholder="회원 유형 선택" class="w-full" />
</div>
<div class="field mb-3">
<label for="phone" class="mb-1">전화번호</label>
<InputText id="phone" v-model="member.phone" class="w-full" />
</div>
<div class="field mb-3">
<label for="email" class="mb-1">이메일</label>
<InputText id="email" v-model="member.email" class="w-full" />
</div>
<div class="field mb-3">
<label for="handicap" class="mb-1">핸디캡</label>
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" class="w-full" />
</div>
<div class="field mb-3">
<label for="status" class="mb-1">상태</label>
<Select id="status" v-model="member.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
<div class="grid">
<div class="field col-12 mb-3">
<label for="name" class="mb-1">이름</label>
<InputText id="name" v-model="member.name" class="w-full" />
</div>
<div class="field col-6 mb-3">
<label for="gender" class="mb-1">성별</label>
<Select id="gender" v-model="member.gender" :options="genderOptions" optionLabel="name" optionValue="code" placeholder="성별 선택" class="w-full" />
</div>
<div class="field col-6 mb-3">
<label for="memberType" class="mb-1">회원 유형</label>
<Select id="memberType" v-model="member.memberType" :options="memberTypes" optionLabel="name" optionValue="code" placeholder="회원 유형 선택" class="w-full" />
</div>
<div class="field col-6 mb-3">
<label for="phone" class="mb-1">전화번호</label>
<InputText id="phone" v-model="member.phone" class="w-full" />
</div>
<div class="field col-6 mb-3">
<label for="email" class="mb-1">이메일</label>
<InputText id="email" v-model="member.email" class="w-full" />
</div>
<div class="field col-6 mb-3">
<label for="handicap" class="mb-1">핸디캡</label>
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" class="w-full" />
</div>
<div class="field col-6 mb-3">
<label for="status" class="mb-1">상태</label>
<Select id="status" v-model="member.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
</div>
</div>
<div class="flex justify-content-end gap-2 mt-4">
<Button label="취소" icon="pi pi-times" class="p-button-text" @click="hideDialog" />
@@ -168,25 +176,27 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { useToast } from 'primevue/usetoast';
import apiClient from '@/services/api';
import clubService from '@/services/clubService';
import { useClubStore } from '@/stores/clubStore';
const toast = useToast();
const clubStore = useClubStore();
// 회원 유형 목록
const memberTypes = ref([
{ name: '정회원', code: 'regular' },
{ name: '준회원', code: 'associate' },
{ name: '운영진', code: 'admin' },
{ name: '모임장', code: 'owner' }
{ name: '정회원', code: '정회원' },
{ name: '준회원', code: '준회원' },
{ name: '운영진', code: '운영진' },
{ name: '모임장', code: '모임장' }
]);
// 성별 옵션
const genderOptions = ref([
{ name: '남성', code: 'male' },
{ name: '여성', code: 'female' }
{ name: '남성', code: '남성' },
{ name: '여성', code: '여성' }
]);
// 회원 목록
@@ -373,9 +383,23 @@ const openNewMemberDialog = () => {
memberDialog.value = true;
};
// 성별 선택 시 핸디캡 자동 설정
watch(() => member.value.gender, (newGender) => {
if (dialogMode.value === 'new' && newGender && clubStore.selectedClub) {
if (newGender.name === '여성' && clubStore.selectedClub.femaleHandicap) {
member.value.handicap = clubStore.selectedClub.femaleHandicap;
} else if (newGender.name === '남성') {
member.value.handicap = 0;
}
}
});
// 회원 수정 다이얼로그 열기
const editMember = (data) => {
member.value = { ...data };
member.value = {
...data
};
dialogMode.value = 'edit';
memberDialog.value = true;
};
@@ -388,11 +412,15 @@ const hideDialog = () => {
// 회원 저장
const saveMember = async () => {
try {
const memberData = {
...member.value
};
if (dialogMode.value === 'new') {
await clubService.addClubMember(clubId.value, member.value);
await clubService.addClubMember(clubId.value, memberData);
toast.add({ severity: 'success', summary: '성공', detail: '새 회원이 추가되었습니다.', life: 3000 });
} else {
await clubService.updateClubMember(clubId.value, member.value.id, member.value);
await clubService.updateClubMember(clubId.value, memberData.id, memberData);
toast.add({ severity: 'success', summary: '성공', detail: '회원 정보가 수정되었습니다.', life: 3000 });
}