diff --git a/backend/controllers/clubController.js b/backend/controllers/clubController.js index 5f207ba..62ae22d 100644 --- a/backend/controllers/clubController.js +++ b/backend/controllers/clubController.js @@ -188,6 +188,36 @@ exports.createClub = async (req, res) => { console.log(`${ownerEmail}에게 초대 이메일 발송 예정`); } + // 베이직 1개월 플랜 자동 추가 + try { + // 베이직 플랜 조회 (ID가 1이라고 가정, 실제 환경에 맞게 조정 필요) + const basicPlan = await require('../models').SubscriptionPlan.findOne({ + where: { name: 'Basic' } + }); + + if (basicPlan) { + const startDate = new Date(); + const endDate = new Date(); + endDate.setMonth(endDate.getMonth() + 1); // 1개월 추가 + + await require('../models').ClubSubscription.create({ + clubId: newClub.id, + planId: basicPlan.id, + startDate, + endDate, + status: 'active', + price: basicPlan.price || 0 + }); + + console.log(`클럽 ${newClub.id}에 베이직 1개월 플랜 자동 추가됨`); + } else { + console.warn('베이직 플랜을 찾을 수 없어 자동 구독이 추가되지 않았습니다.'); + } + } catch (subscriptionError) { + console.error('자동 구독 추가 오류:', subscriptionError); + // 구독 추가 실패해도 클럽 생성은 성공으로 처리 + } + res.status(201).json(newClub); } catch (error) { console.error('클럽 생성 오류:', error); @@ -198,18 +228,24 @@ exports.createClub = async (req, res) => { // 클럽 정보 업데이트 exports.updateClub = async (req, res) => { try { - const { clubId, name, location, description, contact, ownerEmail, status, femaleHandicap } = req.body; + const { clubId, name, location, description, contact, ownerEmail, status, femaleHandicap, ownerId } = req.body; const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } - // 이메일이 변경된 경우 새 모임장 확인 + // 모임장 변경 확인 (직접 ownerId를 전달받거나 이메일로 변경) let newOwnerId = club.ownerId; let ownerChanged = false; - if (ownerEmail && ownerEmail !== club.ownerEmail) { + // ownerId가 직접 전달된 경우 (클럽 설정에서 모임장 선택) + if (ownerId && ownerId !== club.ownerId) { + newOwnerId = ownerId; + ownerChanged = true; + } + // 이메일을 통한 모임장 변경 (기존 기능 유지) + else if (ownerEmail && ownerEmail !== club.ownerEmail) { const newOwner = await User.findOne({ where: { email: ownerEmail } }); if (!newOwner) { return res.status(404).json({ message: '해당 이메일로 등록된 사용자를 찾을 수 없습니다.' }); @@ -229,48 +265,13 @@ exports.updateClub = async (req, res) => { ownerId: newOwnerId }); - // 모임장이 변경된 경우 Members 테이블 업데이트 + // 모임장이 변경된 경우 모임장 변경 메소드 호출 if (ownerChanged) { - // 기존 모임장을 정회원으로 변경 - await Member.update( - { memberType: '정회원' }, - { - where: { - clubId: clubId, - userId: club.ownerId, - memberType: '모임장' - } - } - ); - - // 새 모임장 확인 및 업데이트 - const newOwnerMember = await Member.findOne({ - where: { - clubId: clubId, - userId: newOwnerId - } - }); - - if (newOwnerMember) { - // 기존 회원인 경우 모임장으로 변경 - await newOwnerMember.update({ memberType: '모임장' }); - } else { - // 새로운 사용자 정보 가져오기 - const newOwnerUser = await User.findByPk(newOwnerId); - if (!newOwnerUser) { - return res.status(404).json({ message: '새 모임장 사용자 정보를 찾을 수 없습니다.' }); - } - - // 새 회원으로 모임장 추가 - await Member.create({ - userId: newOwnerId, - clubId: clubId, - name: newOwnerUser.name || ownerEmail.split('@')[0], // 이름이 없는 경우 이메일 아이디 사용 - gender: '남성', // 성별 정보가 없는 경우 기본값 - memberType: '모임장', - joinDate: new Date(), - status: 'active' - }); + try { + // 모임장 변경 내부 메소드 호출 + await changeClubOwnerInternal(clubId, newOwnerId, ownerEmail); + } catch (error) { + throw error; } } @@ -362,6 +363,7 @@ exports.getClubMembers = async (req, res) => { const memberData = member.toJSON(); return { id: memberData.id, + userId: memberData.userId || null, name: memberData.name, gender: memberData.gender, memberType: memberData.memberType, @@ -437,28 +439,17 @@ exports.updateClubMember = async (req, res) => { if (!member) { return res.status(404).json({ message: '회원을 찾을 수 없습니다.' }); } - - // 모임장은 한 명만 존재할 수 있음 - if (memberType === '모임장') { - const existingOwner = await Member.findOne({ - where: { - clubId: clubId, - memberType: '모임장', - status: 'active', - id: { [Op.ne]: memberId } - } - }); - - if (existingOwner) { - return res.status(400).json({ message: '이미 다른 모임장이 존재합니다.' }); - } + + // 모임장으로 변경 금지 - 모임장 변경은 별도 API로 처리 + if (memberType === '모임장' && member.memberType !== '모임장') { + return res.status(403).json({ message: '모임장 변경은 클럽 설정에서만 가능합니다.' }); } - // 회원 정보 업데이트 + // 회원 정보 업데이트 (모임장 유지 또는 다른 유형으로 변경만 허용) await member.update({ name: name || member.name, gender: gender || member.gender, - memberType: memberType || member.memberType, + memberType: (member.memberType === '모임장' && memberType !== '모임장') ? '모임장' : memberType || member.memberType, email: email || member.email, phone: phone || member.phone, handicap: handicap || member.handicap, @@ -479,6 +470,67 @@ exports.updateClubMember = async (req, res) => { } }; +// 모임장 변경 내부 메소드 +const changeClubOwnerInternal = async (clubId, newOwnerId, ownerEmail = null) => { + // 트랜잭션 시작 + const t = await sequelize.transaction(); + + try { + // 기존 모임장을 운영진으로 변경 + await Member.update( + { memberType: '운영진' }, + { + where: { + clubId: clubId, + memberType: '모임장', + status: 'active' + }, + transaction: t + } + ); + + // 새 모임장 확인 및 업데이트 + const newOwnerMember = await Member.findOne({ + where: { + clubId: clubId, + userId: newOwnerId, + status: 'active' + } + }); + + if (newOwnerMember) { + // 기존 회원인 경우 모임장으로 변경 + await newOwnerMember.update({ memberType: '모임장' }, { transaction: t }); + } else { + // 새로운 사용자 정보 가져오기 + const newOwnerUser = await User.findByPk(newOwnerId); + if (!newOwnerUser) { + await t.rollback(); + throw new Error('새 모임장 사용자 정보를 찾을 수 없습니다.'); + } + + // 새 회원으로 모임장 추가 + await Member.create({ + userId: newOwnerId, + clubId: clubId, + name: newOwnerUser.name || (ownerEmail ? ownerEmail.split('@')[0] : ''), // 이름이 없는 경우 이메일 아이디 사용 + gender: '남성', // 성별 정보가 없는 경우 기본값 + memberType: '모임장', + joinDate: new Date(), + status: 'active' + }, { transaction: t }); + } + + // 트랜잭션 커밋 + await t.commit(); + return true; + } catch (error) { + // 오류 발생 시 트랜잭션 롤백 + await t.rollback(); + throw error; + } +}; + // 클럽 회원 삭제 exports.deleteClubMember = async (req, res) => { try { diff --git a/backend/controllers/subscriptionController.js b/backend/controllers/subscriptionController.js index b46173b..c5a4f9e 100644 --- a/backend/controllers/subscriptionController.js +++ b/backend/controllers/subscriptionController.js @@ -461,14 +461,23 @@ exports.requestPayment = async (req, res) => { planChange: planChange || false }); + // 플랜 정보 조회 (planId가 있는 경우) + let planName = ''; + if (planId) { + const plan = await SubscriptionPlan.findByPk(planId); + if (plan) { + planName = plan.name; // 베이직 또는 프리미엄 등의 플랜 이름 + } + } + // 토스페이먼츠에 전송할 데이터 구성 const paymentData = { orderId: paymentInfo.orderId, amount: totalAmount, - orderName: planId ? `구독 플랜: ${planDuration}개월` : '추가 기능 구매', + orderName: planId ? `${planName} 플랜: ${planDuration}개월` : '추가 기능 구매', customerName: club.name, - successUrl: successUrl || `${process.env.FRONTEND_URL}/payment/success`, - failUrl: failUrl || `${process.env.FRONTEND_URL}/payment/fail`, + successUrl: successUrl || `${req.headers.origin}/payment/success`, + failUrl: failUrl || `${req.headers.origin}/payment/fail`, customerEmail: req.user.email }; diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 88fb91c..3269fdd 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,7 +15,7 @@

{{ selectedClub.name }}

- @@ -340,6 +340,19 @@ const loadMenuItems = async () => { } }; +// 사이드바 새로고침 메소드 +const refreshSidebar = async () => { + console.log('사이드바 새로고침 시작...'); + await fetchClubs(); + await fetchClubInfo(); + loadMenuItems(); + initializeExpandedGroups(); // 메뉴 그룹 초기화 + console.log('사이드바 새로고침 완료'); +}; + +// 사이드바 새로고침 메소드를 전역으로 노출 +provide('refreshSidebar', refreshSidebar); + // 사용자 정보 로드 const loadUserData = async () => { const token = localStorage.getItem('token'); diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 80f7062..fddad28 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -47,6 +47,13 @@
+ + +
+
+
-
@@ -194,12 +194,12 @@ import { useToast } from 'primevue/usetoast'; const clubStore = useClubStore(); const toast = useToast(); -// 회원 유형 목록 -const memberTypes = ref([ +// 회원 유형 옵션 +const memberTypeOptions = ref([ { name: '정회원', code: '정회원' }, { name: '준회원', code: '준회원' }, - { name: '운영진', code: '운영진' }, - { name: '모임장', code: '모임장' } + { name: '운영진', code: '운영진' } + // 모임장 옵션은 제거 - 클럽 설정에서만 변경 가능하도록 제한 ]); // 성별 옵션 diff --git a/frontend/src/views/club/subscription/SubscriptionManagement.vue b/frontend/src/views/club/subscription/SubscriptionManagement.vue index 3110bdb..2c2bcff 100644 --- a/frontend/src/views/club/subscription/SubscriptionManagement.vue +++ b/frontend/src/views/club/subscription/SubscriptionManagement.vue @@ -1097,6 +1097,10 @@ const processPayment = async () => { // 토스페이먼츠 SDK 초기화 const tossPayments = await loadTossPaymentsSDK(); + // 결제 성공/실패 핸들러 등록 (전역 함수로 등록) + window.handlePaymentSuccess = handlePaymentSuccess; + window.handlePaymentFail = handlePaymentFail; + // 결제 창 열기 - 결제 방법을 미리 지정하지 않고 토스페이먼츠 결제 창에서 선택하도록 함 await tossPayments.requestPayment({ ...response.data.paymentData, @@ -1118,6 +1122,21 @@ const processPayment = async () => { } }; +// 선택 항목 초기화 +const resetSelections = () => { + selectedPlan.value = null; + selectedFeatures.value = []; + selectedPlanDuration.value = 1; + showPlans.value = false; + showFeatures.value = false; + isSelectingPlan.value = false; + isSelectingFeatures.value = false; + changeOption.value = 'immediate'; + paymentAgreement.value = false; + showPaymentSection.value = false; + showAdditionalFeatures.value = false; +}; + // 토스페이먼츠 SDK 로드 const loadTossPaymentsSDK = () => { return new Promise((resolve, reject) => { @@ -1153,6 +1172,8 @@ const loadTossPaymentsSDK = () => { // 결제 성공 처리 const handlePaymentSuccess = async (orderId) => { try { + console.log('결제 성공:', orderId); + // 데이터 다시 로드 await Promise.all([ loadCurrentSubscription(), @@ -1162,23 +1183,40 @@ const handlePaymentSuccess = async (orderId) => { ]); // 상태 초기화 - selectedPlan.value = null; - selectedFeatures.value = []; - paymentAgreement.value = false; - showPaymentSection.value = false; - showPlans.value = false; - showAdditionalFeatures.value = false; - changeOption.value = isUpgrade.value ? 'immediate' : 'nextBilling'; + resetSelections(); - alert('결제가 성공적으로 완료되었습니다.'); + // 성공 메시지 표시 (토스트 메시지 추가) + toast.add({ + severity: 'success', + summary: '결제 성공', + detail: '구독 플랜 결제가 성공적으로 완료되었습니다.', + life: 5000 + }); } catch (error) { console.error('결제 성공 후 처리 오류:', error); + toast.add({ + severity: 'error', + summary: '오류', + detail: '구독 정보를 새로고침하는 중 오류가 발생했습니다.', + life: 5000 + }); } }; // 결제 실패 처리 const handlePaymentFail = (message) => { - alert(`결제 실패: ${message || '사용자가 결제를 취소했습니다.'}`); + console.log('결제 실패:', message); + + // 실패 메시지 표시 (토스트 메시지 추가) + toast.add({ + severity: 'error', + summary: '결제 실패', + detail: message || '결제 처리 중 오류가 발생했습니다.', + life: 5000 + }); + + // 결제 실패 후 초기화 + resetSelections(); }; // 플랜 구독 (기존 기능 - 레거시)