클럽 설정 기능
This commit is contained in:
@@ -198,8 +198,7 @@ exports.createClub = async (req, res) => {
|
|||||||
// 클럽 정보 업데이트
|
// 클럽 정보 업데이트
|
||||||
exports.updateClub = async (req, res) => {
|
exports.updateClub = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id, name, location, description, contact, ownerEmail, status } = req.body;
|
const { clubId, name, location, description, contact, ownerEmail, status, femaleHandicap } = req.body;
|
||||||
const clubId = id || req.params.id;
|
|
||||||
const club = await Club.findByPk(clubId);
|
const club = await Club.findByPk(clubId);
|
||||||
|
|
||||||
if (!club) {
|
if (!club) {
|
||||||
@@ -226,6 +225,7 @@ exports.updateClub = async (req, res) => {
|
|||||||
description: description !== undefined ? description : club.description,
|
description: description !== undefined ? description : club.description,
|
||||||
contact: contact !== undefined ? contact : club.contact,
|
contact: contact !== undefined ? contact : club.contact,
|
||||||
status: status || club.status,
|
status: status || club.status,
|
||||||
|
femaleHandicap: femaleHandicap || club.femaleHandicap,
|
||||||
ownerId: newOwnerId
|
ownerId: newOwnerId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -421,7 +421,7 @@ exports.addClubMember = async (req, res) => {
|
|||||||
// 클럽 회원 정보 수정
|
// 클럽 회원 정보 수정
|
||||||
exports.updateClubMember = async (req, res) => {
|
exports.updateClubMember = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { clubId, memberId, name, gender, memberType, email, phone, status } = req.body;
|
const { clubId, memberId, name, gender, memberType, email, phone, handicap, status } = req.body;
|
||||||
|
|
||||||
if (!clubId || !memberId) {
|
if (!clubId || !memberId) {
|
||||||
return res.status(400).json({ message: '잘못된 클럽 또는 회원 ID입니다.' });
|
return res.status(400).json({ message: '잘못된 클럽 또는 회원 ID입니다.' });
|
||||||
@@ -461,6 +461,7 @@ exports.updateClubMember = async (req, res) => {
|
|||||||
memberType: memberType || member.memberType,
|
memberType: memberType || member.memberType,
|
||||||
email: email || member.email,
|
email: email || member.email,
|
||||||
phone: phone || member.phone,
|
phone: phone || member.phone,
|
||||||
|
handicap: handicap || member.handicap,
|
||||||
status: status || member.status
|
status: status || member.status
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ exports.addPublicGuest = async (req, res) => {
|
|||||||
// 클럽의 회원 수와 구독 플랜의 최대 회원 수 확인
|
// 클럽의 회원 수와 구독 플랜의 최대 회원 수 확인
|
||||||
const memberCount = await Member.count({ where: { clubId: event.clubId } });
|
const memberCount = await Member.count({ where: { clubId: event.clubId } });
|
||||||
|
|
||||||
|
// 클럽 정보 가져오기 (여성 기본 핸디캡 확인을 위해)
|
||||||
|
const club = await Club.findByPk(event.clubId);
|
||||||
|
if (!club) return res.status(404).json({ message: '클럽 정보를 찾을 수 없습니다.' });
|
||||||
|
|
||||||
// 클럽의 구독 정보 가져오기 - 가장 최근 구독 정보
|
// 클럽의 구독 정보 가져오기 - 가장 최근 구독 정보
|
||||||
const clubSubscription = await sequelize.models.ClubSubscription.findOne({
|
const clubSubscription = await sequelize.models.ClubSubscription.findOne({
|
||||||
where: { clubId: event.clubId },
|
where: { clubId: event.clubId },
|
||||||
@@ -288,6 +292,12 @@ exports.addPublicGuest = async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 여성 핸디캡 적용
|
||||||
|
let handicap = null;
|
||||||
|
if (gender === '여성' && club.femaleHandicap) {
|
||||||
|
handicap = club.femaleHandicap;
|
||||||
|
}
|
||||||
|
|
||||||
// publicAuth 미들웨어로 인증됨
|
// publicAuth 미들웨어로 인증됨
|
||||||
const member = await Member.create({
|
const member = await Member.create({
|
||||||
name,
|
name,
|
||||||
@@ -295,7 +305,8 @@ exports.addPublicGuest = async (req, res) => {
|
|||||||
gender,
|
gender,
|
||||||
clubId: event.clubId,
|
clubId: event.clubId,
|
||||||
memberType: '게스트',
|
memberType: '게스트',
|
||||||
average: average || 0
|
average: average || 0,
|
||||||
|
handicap: handicap // 여성 핸디캡 적용
|
||||||
});
|
});
|
||||||
const participant = await EventParticipant.create({ eventId: event.id, memberId: member.id });
|
const participant = await EventParticipant.create({ eventId: event.id, memberId: member.id });
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ const Club = sequelize.define('Club', {
|
|||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '회원 수'
|
comment: '회원 수'
|
||||||
},
|
},
|
||||||
|
femaleHandicap: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 15,
|
||||||
|
comment: '여성 기본 핸디캡'
|
||||||
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
+27
-1
@@ -12,7 +12,15 @@
|
|||||||
<!-- 클럽 정보 -->
|
<!-- 클럽 정보 -->
|
||||||
<div class="club-info" v-if="selectedClub">
|
<div class="club-info" v-if="selectedClub">
|
||||||
<div class="club-header">
|
<div class="club-header">
|
||||||
<h3>{{ selectedClub.name }}</h3>
|
<div class="club-title">
|
||||||
|
<h3>{{ selectedClub.name }}</h3>
|
||||||
|
<router-link v-if="userRole === 'admin' || userRole === '모임장' || userRole === '운영진'"
|
||||||
|
:to="{ name: 'ClubSettings' }"
|
||||||
|
class="settings-icon"
|
||||||
|
title="클럽 설정">
|
||||||
|
<i class="pi pi-cog"></i>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
<p class="club-location"><i class="pi pi-map-marker"></i> {{ selectedClub.location }}</p>
|
<p class="club-location"><i class="pi pi-map-marker"></i> {{ selectedClub.location }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="club-stats">
|
<div class="club-stats">
|
||||||
@@ -121,6 +129,7 @@ const ownerName = ref('');
|
|||||||
const isRouterReady = ref(false);
|
const isRouterReady = ref(false);
|
||||||
const userRole = computed(() => user.value?.role || '');
|
const userRole = computed(() => user.value?.role || '');
|
||||||
const isAdmin = computed(() => userRole.value === 'admin' || userRole.value === 'superadmin');
|
const isAdmin = computed(() => userRole.value === 'admin' || userRole.value === 'superadmin');
|
||||||
|
const userClubRole = ref(localStorage.getItem('userClubRole') || '');
|
||||||
|
|
||||||
// 화면 크기 변경 감지
|
// 화면 크기 변경 감지
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -430,12 +439,29 @@ body {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.club-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.club-header h3 {
|
.club-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 1.1em;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon:hover {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
.club-location {
|
.club-location {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const EventManagement = () => import('@/views/club/EventManagement.vue');
|
|||||||
const EventCalendar = () => import('@/views/club/EventCalendar.vue');
|
const EventCalendar = () => import('@/views/club/EventCalendar.vue');
|
||||||
const ScoreManagement = () => import('@/views/club/ScoreManagement.vue');
|
const ScoreManagement = () => import('@/views/club/ScoreManagement.vue');
|
||||||
const UserClubCreate = () => import('@/views/club/ClubCreate.vue');
|
const UserClubCreate = () => import('@/views/club/ClubCreate.vue');
|
||||||
|
const ClubSettings = () => import('@/views/club/ClubSettings.vue');
|
||||||
|
|
||||||
// 구독 관리 컴포넌트 import
|
// 구독 관리 컴포넌트 import
|
||||||
const ClubSubscriptionManagement = () => import('@/views/club/subscription/SubscriptionManagement.vue');
|
const ClubSubscriptionManagement = () => import('@/views/club/subscription/SubscriptionManagement.vue');
|
||||||
@@ -133,6 +134,15 @@ const routes = [
|
|||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiredRoles: ['admin', '모임장', '운영진']
|
requiredRoles: ['admin', '모임장', '운영진']
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
name: 'ClubSettings',
|
||||||
|
component: ClubSettings,
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiredRoles: ['admin', '모임장', '운영진']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -201,8 +211,9 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 클럽 구독 관리 페이지에 접근하려는 경우, 권한 확인
|
// 클럽 구독 관리 또는 설정 페이지에 접근하려는 경우, 권한 확인
|
||||||
if (to.name === 'ClubSubscription' && (userClubRole !== 'admin' && userClubRole !== '모임장' && userClubRole !== '운영진')) {
|
if ((to.name === 'ClubSubscription' || to.name === 'ClubSettings') &&
|
||||||
|
(userClubRole !== 'admin' && userClubRole !== '모임장' && userClubRole !== '운영진')) {
|
||||||
next({ name: 'ClubDashboard' });
|
next({ name: 'ClubDashboard' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
stripedRows
|
||||||
filterDisplay="menu"
|
filterDisplay="menu"
|
||||||
v-model:filters="filters"
|
v-model:filters="filters"
|
||||||
|
sort-field="startDate"
|
||||||
|
:sort-order="-1"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
>
|
>
|
||||||
<Column field="eventType" header="유형" sortable style="width: 10%">
|
<Column field="eventType" header="유형" sortable style="width: 10%">
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
<DataTable :value="filteredMembers" :paginator="true" :rows="10"
|
<DataTable :value="filteredMembers" :paginator="true" :rows="10"
|
||||||
:rowsPerPageOptions="[5, 10, 20]"
|
:rowsPerPageOptions="[5, 10, 20]"
|
||||||
|
sortField="name"
|
||||||
|
:sortOrder="1"
|
||||||
stripedRows responsiveLayout="scroll">
|
stripedRows responsiveLayout="scroll">
|
||||||
<Column field="name" header="이름" sortable style="width: 15%"></Column>
|
<Column field="name" header="이름" sortable style="width: 15%"></Column>
|
||||||
<Column field="gender" header="성별" sortable style="width: 10%"></Column>
|
<Column field="gender" header="성별" sortable style="width: 10%"></Column>
|
||||||
@@ -62,29 +64,35 @@
|
|||||||
:header="dialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
|
:header="dialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
|
||||||
:modal="true" class="p-fluid">
|
:modal="true" class="p-fluid">
|
||||||
<!-- 회원 정보 다이얼로그 내부 폼 예시 -->
|
<!-- 회원 정보 다이얼로그 내부 폼 예시 -->
|
||||||
<div class="field mb-3">
|
<div class="grid">
|
||||||
<label for="gender" class="mb-1">성별</label>
|
<div class="field col-12 mb-3">
|
||||||
<Select id="gender" v-model="member.gender" :options="genderOptions" optionLabel="name" placeholder="성별 선택" class="w-full" />
|
<label for="name" class="mb-1">이름</label>
|
||||||
</div>
|
<InputText id="name" v-model="member.name" class="w-full" />
|
||||||
<div class="field mb-3">
|
</div>
|
||||||
<label for="memberType" class="mb-1">회원 유형</label>
|
<div class="field col-6 mb-3">
|
||||||
<Select id="memberType" v-model="member.memberTypeObj" :options="memberTypes" optionLabel="name" placeholder="회원 유형 선택" class="w-full" />
|
<label for="gender" class="mb-1">성별</label>
|
||||||
</div>
|
<Select id="gender" v-model="member.gender" :options="genderOptions" optionLabel="name" optionValue="code" placeholder="성별 선택" class="w-full" />
|
||||||
<div class="field mb-3">
|
</div>
|
||||||
<label for="phone" class="mb-1">전화번호</label>
|
<div class="field col-6 mb-3">
|
||||||
<InputText id="phone" v-model="member.phone" class="w-full" />
|
<label for="memberType" class="mb-1">회원 유형</label>
|
||||||
</div>
|
<Select id="memberType" v-model="member.memberType" :options="memberTypes" optionLabel="name" optionValue="code" placeholder="회원 유형 선택" class="w-full" />
|
||||||
<div class="field mb-3">
|
</div>
|
||||||
<label for="email" class="mb-1">이메일</label>
|
<div class="field col-6 mb-3">
|
||||||
<InputText id="email" v-model="member.email" class="w-full" />
|
<label for="phone" class="mb-1">전화번호</label>
|
||||||
</div>
|
<InputText id="phone" v-model="member.phone" class="w-full" />
|
||||||
<div class="field mb-3">
|
</div>
|
||||||
<label for="handicap" class="mb-1">핸디캡</label>
|
<div class="field col-6 mb-3">
|
||||||
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" class="w-full" />
|
<label for="email" class="mb-1">이메일</label>
|
||||||
</div>
|
<InputText id="email" v-model="member.email" class="w-full" />
|
||||||
<div class="field mb-3">
|
</div>
|
||||||
<label for="status" class="mb-1">상태</label>
|
<div class="field col-6 mb-3">
|
||||||
<Select id="status" v-model="member.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
|
<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>
|
||||||
<div class="flex justify-content-end gap-2 mt-4">
|
<div class="flex justify-content-end gap-2 mt-4">
|
||||||
<Button label="취소" icon="pi pi-times" class="p-button-text" @click="hideDialog" />
|
<Button label="취소" icon="pi pi-times" class="p-button-text" @click="hideDialog" />
|
||||||
@@ -168,25 +176,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue';
|
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import apiClient from '@/services/api';
|
import apiClient from '@/services/api';
|
||||||
import clubService from '@/services/clubService';
|
import clubService from '@/services/clubService';
|
||||||
|
import { useClubStore } from '@/stores/clubStore';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const clubStore = useClubStore();
|
||||||
|
|
||||||
// 회원 유형 목록
|
// 회원 유형 목록
|
||||||
const memberTypes = ref([
|
const memberTypes = ref([
|
||||||
{ name: '정회원', code: 'regular' },
|
{ name: '정회원', code: '정회원' },
|
||||||
{ name: '준회원', code: 'associate' },
|
{ name: '준회원', code: '준회원' },
|
||||||
{ name: '운영진', code: 'admin' },
|
{ name: '운영진', code: '운영진' },
|
||||||
{ name: '모임장', code: 'owner' }
|
{ name: '모임장', code: '모임장' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 성별 옵션
|
// 성별 옵션
|
||||||
const genderOptions = ref([
|
const genderOptions = ref([
|
||||||
{ name: '남성', code: 'male' },
|
{ name: '남성', code: '남성' },
|
||||||
{ name: '여성', code: 'female' }
|
{ name: '여성', code: '여성' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 회원 목록
|
// 회원 목록
|
||||||
@@ -373,9 +383,23 @@ const openNewMemberDialog = () => {
|
|||||||
memberDialog.value = true;
|
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) => {
|
const editMember = (data) => {
|
||||||
member.value = { ...data };
|
member.value = {
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
|
||||||
dialogMode.value = 'edit';
|
dialogMode.value = 'edit';
|
||||||
memberDialog.value = true;
|
memberDialog.value = true;
|
||||||
};
|
};
|
||||||
@@ -388,11 +412,15 @@ const hideDialog = () => {
|
|||||||
// 회원 저장
|
// 회원 저장
|
||||||
const saveMember = async () => {
|
const saveMember = async () => {
|
||||||
try {
|
try {
|
||||||
|
const memberData = {
|
||||||
|
...member.value
|
||||||
|
};
|
||||||
|
|
||||||
if (dialogMode.value === 'new') {
|
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 });
|
toast.add({ severity: 'success', summary: '성공', detail: '새 회원이 추가되었습니다.', life: 3000 });
|
||||||
} else {
|
} 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 });
|
toast.add({ severity: 'success', summary: '성공', detail: '회원 정보가 수정되었습니다.', life: 3000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user